diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..fb74de6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.3 + - 1.4 + - 1.5 + - tip diff --git a/LICENSE b/LICENSE index 4909176..5641b89 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Fog Creek Software +Copyright (c) 2015 Blake Caldwell Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 782e67c..5a83618 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,83 @@ -Web-Based Memory Profiler for Go Services +Web-Based Memory Profiler for Go Services [![Build Status](https://travis-ci.org/wblakecaldwell/profiler.svg?branch=master)](https://travis-ci.org/wblakecaldwell/profiler) [![GoDoc](https://godoc.org/github.com/wblakecaldwell/profiler?status.svg)](https://godoc.org/github.com/wblakecaldwell/profiler) ========================================= -This package helps you track your service's memory usage and report custom properties. +Profiler helps you track your service's memory usage and custom key/value diagnostic info. -*TODO: more info, screenshots, examples* +![Profiler Screenshot](screenshot.png) Enabling Memory Profiling ------------------------- -To enable memory profiling, modify your main method like this: - - import ( - "net/http" - "github.com/fogcreek/profiler" - ) - func main() { - // add handlers to help us track memory usage - they don't track memory until they're told to - profiler.AddMemoryProfilingHandlers() - - // listen on port 6060 (pick a port) - http.ListenAndServe(":6060", nil) - } - +The simplest way to use the profiler is to add its endpoints to your HTTP listener. +See the [extra_service_info](examples/extra_service_info/) example for how to +serve the profiler's endpoints on its own IP/port. + +```go +import ( + "net/http" + "github.com/wblakecaldwell/profiler" +) +func main() { + // add the profiler handler endpoints + profiler.AddMemoryProfilingHandlers() + + // add realtime extra key/value diagnostic info (optional) + profiler.RegisterExtraServiceInfoRetriever(extraServiceInfo) + + // start the profiler on service start (optional) + profiler.StartProfiling() + + // listen on port 6060 (pick a port) + http.ListenAndServe(":6060", nil) +} + +// extraServiceInfo returns key/value diagnostic info +func extraServiceInfo() map[string]interface{} { + extraInfo := make(map[string]interface{}) + extraInfo["uptime"] = fetchUptime() + extraInfo["successful connection count"] = fetchSuccessfulConnectionCount() + extraInfo["failure connection count"] = fetchFailureConnectionCount() + return extraInfo +} +``` Using Memory Profiling ---------------------- Enabling Memory Profiling exposes the following endpoints: -- http://localhost:6060/profiler/memstats: Main page you should visit +- http://localhost:6060/profiler/stop : Stop recording memory statistics +- http://localhost:6060/profiler/start : Start recording memory statistics +- http://localhost:6060/profiler/info.html : Main page you should visit +- http://localhost:6060/profiler/info : JSON data that feeds profiler/info.html + + +Examples +-------- -- http://localhost:6060/profiler/stop: Stop recording memory statistics +View and/or run the three working examples in the [examples](examples/) folder: -- http://localhost:6060/profiler/start: Start recording memory statistics +1. [Simple](examples/simple/): Serving the profiler's endpoints to a service's single HTTP IP:port +2. [Separate Port](examples/separate_port/): Serving the profiler's endpoints on its own IP:port +3. [Extra Service Info](examples/extra_service_info/): Same as the previous example, with the profiler also reporting diagnostic key/value pairs Working With the Template Files ------------------------------- -We bundle the template files in the Go binary with the 'go-bindata' tool. Everything in -github.com/fogcreek/profiler/profiler-web is bundled up into github.com/fogcreek/profiler/profiler-web.go +Template files are bundled in the Go binary with the 'go-bindata' tool. Everything in +github.com/wblakecaldwell/profiler/profiler-web is bundled up into github.com/wblakecaldwell/profiler/profiler-web.go with the command, assuming your repository is in $GOPATH/src. Production Code Generation (Check this in): - go get github.com/jteeuwen/go-bindata/... - go install github.com/jteeuwen/go-bindata/go-bindata +```shell +go get github.com/jteeuwen/go-bindata/... +go install github.com/jteeuwen/go-bindata/go-bindata - go-bindata -prefix "$GOPATH/src/github.com/fogcreek/profiler/profiler-web/" -pkg "profiler" -nocompress -o "$GOPATH/src/github.com/fogcreek/profiler/profiler-web.go" "$GOPATH/src/github.com/fogcreek/profiler/profiler-web" +go-bindata -prefix "$GOPATH/src/github.com/wblakecaldwell/profiler/profiler-web/" -pkg "profiler" -nocompress -o "$GOPATH/src/github.com/wblakecaldwell/profiler/profiler-web.go" "$GOPATH/src/github.com/wblakecaldwell/profiler/profiler-web" +``` If you'd like to make changes to the templates, then use 'go-bindata' in debug mode. Instead of compiling the contents of the template files into profiler-web.go, it generates code to read the content of the template @@ -57,6 +86,8 @@ refresh the browser to see them: Development Code Generation: - go-bindata -debug -prefix "$GOPATH/src/github.com/fogcreek/profiler/profiler-web/" -pkg "profiler" -nocompress -o "$GOPATH/src/github.com/fogcreek/profiler/profiler-web.go" "$GOPATH/src/github.com/fogcreek/profiler/profiler-web" +```shell +go-bindata -debug -prefix "$GOPATH/src/github.com/wblakecaldwell/profiler/profiler-web/" -pkg "profiler" -nocompress -o "$GOPATH/src/github.com/wblakecaldwell/profiler/profiler-web.go" "$GOPATH/src/github.com/wblakecaldwell/profiler/profiler-web" +``` When you've wrapped up development, make sure to rebuild profiler-web.go to contain the contents of the file with the first non-debug command. diff --git a/doc.go b/doc.go index 512eaa5..7b2d6dc 100644 --- a/doc.go +++ b/doc.go @@ -1,60 +1,4 @@ /* Package profiler contains tools to help profile a running Go service. - -Enabling Memory Profiling -------------------------- - -To enable memory profiling, modify your main method like this: - - import ( - "net/http" - "github.com/fogcreek/profiler" - ) - func main() { - // listen on port 6060 (pick a port) - http.ListenAndServe(6060, nil) - - // add handlers to help us track memory usage - they don't track memory until they're told to - profiler.AddMemoryProfilingHandlers() - } - - -Using Memory Profiling ----------------------- - -Enabling Memory Profiling exposes the following endpoints: - -- http://localhost:6060/profiler/memstats: Main page you should visit - -- http://localhost:6060/profiler/stop: Stop recording memory statistics - -- http://localhost:6060/profiler/start: Start recording memory statistics - - -Working With the Template Files -------------------------------- - -We bundle the template files in the Go binary with the 'go-bindata' tool. Everything in -github.com/fogcreek/profiler/profiler-web is bundled up into github.com/fogcreek/profiler/profiler-web.go -with the command, assuming your repository is in $GOPATH/src. - -Production Code Generation (Check this in): - - go get github.com/jteeuwen/go-bindata/... - go install github.com/jteeuwen/go-bindata/go-bindata - - go-bindata -prefix "$GOPATH/src/github.com/fogcreek/profiler/profiler-web/" -pkg "profiler" -nocompress -o "$GOPATH/src/github.com/fogcreek/profiler/profiler-web.go" "$GOPATH/src/github.com/fogcreek/profiler/profiler-web" - -If you'd like to make changes to the templates, then use 'go-bindata' in debug mode. Instead of compiling -the contents of the template files into profiler-web.go, it generates code to read the content of the template -files as they exist at that moment. This way, you can start your service, view the page, make changes, then -refresh the browser to see them: - -Development Code Generation: - - go-bindata -debug -prefix "$GOPATH/src/github.com/fogcreek/profiler/profiler-web/" -pkg "profiler" -nocompress -o "$GOPATH/src/github.com/fogcreek/profiler/profiler-web.go" "$GOPATH/src/github.com/fogcreek/profiler/profiler-web" - -When you've wrapped up development, make sure to rebuild profiler-web.go to contain the contents of the file with the first non-debug command. - */ package profiler diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c19fbaf --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +Examples +======== + +View and/or run the three working examples: + +1. [Simple](simple/): Serving the profiler's endpoints on the same IP:port as the web service +2. [Separate Port](separate_port/): Serving the profiler's endpoints on its own IP:port +3. [Extra Service Info](extra_service_info/): Same as the previous example, with the profiler also reporting diagnostic key/value pairs diff --git a/examples/extra_service_info/README.md b/examples/extra_service_info/README.md new file mode 100644 index 0000000..784ed3a --- /dev/null +++ b/examples/extra_service_info/README.md @@ -0,0 +1,45 @@ +Example: Sharing Extra Service Information +========================================== + +The profiler not only shows you the heap memory usage, but it also gives +your service the opportunity to share some extra diagnostic information as +key/value pairs. You supply the profiler with a function that returns a `map`, +and the profiler will call it when someone is viewing its web page. + +```go +// extraServiceInfo implements the profiler.ExtraServiceInfoRetriever interface, +// returning a map of key/value pairs of diagnostic information. +func extraServiceInfo() map[string]interface{} { + extraInfo := make(map[string]interface{}) + extraInfo["hello"] = "world" + extraInfo["good number"] = 42 + return extraInfo +} + +func main() { + // set up the profiler + // ... + + // give the profiler a function that returns key/value pairs of extra service info + profiler.RegisterExtraServiceInfoRetriever(extraServiceInfo) +} +``` + + +Run the Example +--------------- + +Fetch, build, and run the example service: + +```shell +go get github.com/wblakecaldwell/profiler +go build github.com/wblakecaldwell/profiler/examples/extra_service_info +./extra_service_info +``` + +Verify the 'Hello, World!' endpoint at http://localhost:8080 + +Verify the profiler is running on its own port at http://localhost:6060/profiler/info.html + +![Screenshot](screenshot.png) + diff --git a/examples/extra_service_info/main.go b/examples/extra_service_info/main.go new file mode 100644 index 0000000..04089cf --- /dev/null +++ b/examples/extra_service_info/main.go @@ -0,0 +1,80 @@ +// Example service with the profiler listening on a separate port, +// and sharing extra diagnostic info as key/value pairs +package main + +import ( + "fmt" + "github.com/wblakecaldwell/profiler" + "log" + "net/http" + "sync/atomic" + "time" +) + +// variables for extra service info +var ( + // startTime is the time that the service started + startTime time.Time + + // infoHtmlHitCount keeps track of how many times /profiler/info.html is hit + infoHTMLHitCount uint64 + + // infoHitCount keeps track of how many times the /profiler/info is hit + infoHitCount uint64 +) + +func init() { + startTime = time.Now().Round(time.Second) +} + +func main() { + // start the profiler on its own port + setupProfiler(":6060") + + // provide the profiler with a function where it can request key/value pairs as extra service info + profiler.RegisterExtraServiceInfoRetriever(extraServiceInfo) + + // Serve your public HTTP endpoints on port 8080 + http.HandleFunc("/", helloHandler) + log.Println("Starting server on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +// simple handler that just says Hello +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +// Set up the profiler to listen on the input port:IP string +func setupProfiler(listen string) { + mux := http.NewServeMux() + + // wrap /profiler/info.html for hit tracking + mux.HandleFunc("/profiler/info.html", func(w http.ResponseWriter, r *http.Request) { + atomic.AddUint64(&infoHTMLHitCount, 1) + profiler.MemStatsHTMLHandler(w, r) + }) + + // wrap /profiler/info for hit tracking + mux.HandleFunc("/profiler/info", func(w http.ResponseWriter, r *http.Request) { + atomic.AddUint64(&infoHitCount, 1) + profiler.ProfilingInfoJSONHandler(w, r) + }) + + mux.HandleFunc("/profiler/start", profiler.StartProfilingHandler) + mux.HandleFunc("/profiler/stop", profiler.StopProfilingHandler) + log.Printf("Starting profiler on %s\n", listen) + go func() { + log.Fatal(http.ListenAndServe(listen, mux)) + }() +} + +// extraServiceInfo implements the profiler.ExtraServiceInfoRetriever interface, +// returning a map of key/value pairs of diagnostic information. +func extraServiceInfo() map[string]interface{} { + extraInfo := make(map[string]interface{}) + extraInfo["uptime"] = time.Now().Round(time.Second).Sub(startTime).String() + extraInfo["hit count: /profiler/info.html"] = atomic.LoadUint64(&infoHTMLHitCount) + extraInfo["hit count: /profiler/info"] = atomic.LoadUint64(&infoHitCount) + return extraInfo +} diff --git a/examples/extra_service_info/screenshot.png b/examples/extra_service_info/screenshot.png new file mode 100644 index 0000000..b37ad3f Binary files /dev/null and b/examples/extra_service_info/screenshot.png differ diff --git a/examples/separate_port/README.md b/examples/separate_port/README.md new file mode 100644 index 0000000..df66813 --- /dev/null +++ b/examples/separate_port/README.md @@ -0,0 +1,41 @@ +Example: Profiler on a Separate Port +==================================== + +If you're adding the profiler to a service that serves HTTP endpoints, you might +want the profiler listening on its own IP/port. This makes it easier to keep +your profiling information private, and set up firewall rules to not serve this +IP/port publicly. + +The trick here is to not use the DefaultServeMux. This is all you need to add to +your project to set up the profiler to listen on its own port: + +```go +// Set up the profiler to listen on the input port:IP string +func setupProfiler(listen string) { + mux := http.NewServeMux() + mux.HandleFunc("/profiler/info.html", profiler.MemStatsHTMLHandler) + mux.HandleFunc("/profiler/info", profiler.ProfilingInfoJSONHandler) + mux.HandleFunc("/profiler/start", profiler.StartProfilingHandler) + mux.HandleFunc("/profiler/stop", profiler.StopProfilingHandler) + fmt.Printf("Starting profiler on %s\n", listen) + go http.ListenAndServe(listen, mux) +} +``` + + +Run the Example +--------------- + +Fetch, build, and run the example service: + +```shell +go get github.com/wblakecaldwell/profiler +go build github.com/wblakecaldwell/profiler/examples/separate_port +./separate_port +``` + +Verify the 'Hello, World!' endpoint at http://localhost:8080 + +Verify the profiler is running on its own port at http://localhost:6060/profiler/info.html + +![Screenshot](screenshot.png) diff --git a/examples/separate_port/main.go b/examples/separate_port/main.go new file mode 100644 index 0000000..a6fc5a5 --- /dev/null +++ b/examples/separate_port/main.go @@ -0,0 +1,38 @@ +// Example service with the profiler listening on a separate port from other HTTP endpoints +package main + +import ( + "fmt" + "github.com/wblakecaldwell/profiler" + "log" + "net/http" +) + +func main() { + // start the profiler on its own port + setupProfiler(":6060") + + // Serve your public HTTP endpoints on port 8080 + http.HandleFunc("/", helloHandler) + log.Println("Starting server on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +// simple handler that just says Hello +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} + +// Set up the profiler to listen on the input port:IP string +func setupProfiler(listen string) { + mux := http.NewServeMux() + mux.HandleFunc("/profiler/info.html", profiler.MemStatsHTMLHandler) + mux.HandleFunc("/profiler/info", profiler.ProfilingInfoJSONHandler) + mux.HandleFunc("/profiler/start", profiler.StartProfilingHandler) + mux.HandleFunc("/profiler/stop", profiler.StopProfilingHandler) + + log.Printf("Starting profiler on %s\n", listen) + go func() { + log.Fatal(http.ListenAndServe(listen, mux)) + }() +} diff --git a/examples/separate_port/screenshot.png b/examples/separate_port/screenshot.png new file mode 100644 index 0000000..c6a6544 Binary files /dev/null and b/examples/separate_port/screenshot.png differ diff --git a/examples/simple/README.md b/examples/simple/README.md new file mode 100644 index 0000000..9634f95 --- /dev/null +++ b/examples/simple/README.md @@ -0,0 +1,27 @@ +Example: Simple +=============== + +Here's a simple example, using the least amount of effort possible. The profiler is using +the same IP:port as the web service. The only addition we need is: + +```go +// add handlers to help us track memory usage - they don't track memory until they're told to +profiler.AddMemoryProfilingHandlers() +``` + +Run the Example +--------------- + +Fetch, build, and run the example service: + +```shell +go get github.com/wblakecaldwell/profiler +go build github.com/wblakecaldwell/profiler/examples/simple +./simple +``` + +Verify the "Hello, World!" endpoint at http://localhost:8080 + +Verify the profiler is running at http://localhost:8080/profiler/info.html + +![Screenshot](screenshot.png) diff --git a/examples/simple/main.go b/examples/simple/main.go new file mode 100644 index 0000000..2b61521 --- /dev/null +++ b/examples/simple/main.go @@ -0,0 +1,26 @@ +// Example service with the profiler listening on its only HTTP IP:port +package main + +import ( + "fmt" + "github.com/wblakecaldwell/profiler" + "log" + "net/http" +) + +func main() { + // add our "Hello, World!" endpoint to the default ServeMux + http.HandleFunc("/", helloHandler) + + // add the profiler endpoints to the default ServeMux + profiler.AddMemoryProfilingHandlers() + + // start the service + log.Println("Starting service on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +// simple handler that just says Hello +func helloHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, World!") +} diff --git a/examples/simple/screenshot.png b/examples/simple/screenshot.png new file mode 100644 index 0000000..c6a6544 Binary files /dev/null and b/examples/simple/screenshot.png differ diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..b37ad3f Binary files /dev/null and b/screenshot.png differ diff --git a/web_endpoints.go b/web_endpoints.go index ff97539..87de086 100644 --- a/web_endpoints.go +++ b/web_endpoints.go @@ -101,16 +101,26 @@ func AddMemoryProfilingHandlers() { http.HandleFunc("/profiler/stop", StopProfilingHandler) } +// StartProfiling is a function to start profiling automatically without web button +func StartProfiling() { + commandChannel <- startTracking +} + +// StopProfiling is a function to stop profiling automatically without web button +func StopProfiling() { + commandChannel <- stopTracking +} + // StartProfilingHandler is a HTTP Handler to start memory profiling, if we're not already func StartProfilingHandler(w http.ResponseWriter, r *http.Request) { - commandChannel <- startTracking + StartProfiling() time.Sleep(500 * time.Millisecond) http.Redirect(w, r, "/profiler/info.html", http.StatusTemporaryRedirect) } // StopProfilingHandler is a HTTP Handler to stop memory profiling, if we're profiling func StopProfilingHandler(w http.ResponseWriter, r *http.Request) { - commandChannel <- stopTracking + StopProfiling() time.Sleep(500 * time.Millisecond) http.Redirect(w, r, "/profiler/info.html", http.StatusTemporaryRedirect) }