Skip to content

Commit

Permalink
Refactored URLs handling
Browse files Browse the repository at this point in the history
  • Loading branch information
divan committed Jul 10, 2015
1 parent 60887e4 commit dff1519
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 72 deletions.
10 changes: 7 additions & 3 deletions expvars.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"errors"
"io"
"net/http"
"net/url"
"os"
"time"

"github.com/antonholmquist/jason"
)

// ExpvarsUrl is the default url for fetching expvar info.
const ExpvarsURL = "/debug/vars"
const ExpvarsPath = "/debug/vars"

// Expvar represents fetched expvar variable.
type Expvar struct {
Expand All @@ -23,13 +24,16 @@ func getBasicAuthEnv() (user, password string) {
}

// FetchExpvar fetches expvar by http for the given addr (host:port)
func FetchExpvar(addr string) (*Expvar, error) {
func FetchExpvar(u url.URL) (*Expvar, error) {
e := &Expvar{&jason.Object{}}
client := &http.Client{
Timeout: 1 * time.Second, // TODO: make it configurable or left default?
}

req, _ := http.NewRequest("GET", addr, nil)
req, _ := http.NewRequest("GET", "localhost", nil)
req.URL = &u
req.Host = u.Host

if user, pass := getBasicAuthEnv(); user != "" && pass != "" {
req.SetBasicAuth(user, pass)
}
Expand Down
9 changes: 3 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,25 @@ import (
)

var (
urls = &StringArray{}
interval = flag.Duration("i", 5*time.Second, "Polling interval")
portsArg = flag.String("ports", "", "Ports for accessing services expvars (start-end,port2,port3)")
urls = flag.String("ports", "", "Ports for accessing services expvars (start-end,port2,port3)")
varsArg = flag.String("vars", "mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,memstats.EnableGC,memstats.NumGC,duration:memstats.PauseTotalNs", "Vars to monitor (comma-separated)")
dummy = flag.Bool("dummy", false, "Use dummy (console) output")
self = flag.Bool("self", false, "Monitor itself")
)

func main() {
flag.Var(urls, "url", "urls to poll for expvars")
flag.Usage = Usage
flag.Parse()

// Process ports
ports, _ := ParsePorts(*portsArg)
// Process ports/urls
ports, _ := ParsePorts(*urls)
if *self {
port, err := StartSelfMonitor()
if err == nil {
ports = append(ports, port)
}
}
ports = append(ports, *urls...)
if len(ports) == 0 {
fmt.Fprintln(os.Stderr, "no ports specified. Use -ports arg to specify ports of Go apps to monitor")
Usage()
Expand Down
8 changes: 5 additions & 3 deletions self.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"runtime"
"time"
)
Expand All @@ -27,7 +28,7 @@ const startPort = 32768
// StartSelfMonitor starts http server on random port and exports expvars.
//
// It tries 1024 ports, starting from startPort and registers some expvars if ok.
func StartSelfMonitor() (string, error) {
func StartSelfMonitor() (url.URL, error) {
for port := startPort; port < startPort+1024; port++ {
bind := fmt.Sprintf("localhost:%d", port)
l, err := net.Listen("tcp", bind)
Expand All @@ -39,8 +40,9 @@ func StartSelfMonitor() (string, error) {
expvar.Publish("Goroutines", expvar.Func(goroutines))
expvar.Publish("Uptime", expvar.Func(uptime))
go http.ListenAndServe(bind, nil)
return bind, nil

return NewURL(fmt.Sprintf("%d", port)), nil
}

return "", fmt.Errorf("no free ports found")
return url.URL{}, fmt.Errorf("no free ports found")
}
35 changes: 6 additions & 29 deletions service.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package main

import (
"fmt"
"net"
"strconv"
"net/url"
"strings"
"sync"

Expand All @@ -20,7 +18,7 @@ var (

// Service represents constantly updating info about single service.
type Service struct {
Port string
URL url.URL
Name string
Cmdline string

Expand All @@ -32,15 +30,15 @@ type Service struct {
}

// NewService returns new Service object.
func NewService(port string, vars []VarName) *Service {
func NewService(url url.URL, vars []VarName) *Service {
values := make(map[VarName]*Stack)
for _, name := range vars {
values[VarName(name)] = NewStack()
}

return &Service{
Name: port, // we have only port on start, so use it as name until resolved
Port: port,
Name: url.Host, // we have only port on start, so use it as name until resolved
URL: url,

stacks: values,
}
Expand All @@ -49,7 +47,7 @@ func NewService(port string, vars []VarName) *Service {
// Update updates Service info from Expvar variable.
func (s *Service) Update(wg *sync.WaitGroup) {
defer wg.Done()
expvar, err := FetchExpvar(s.Addr())
expvar, err := FetchExpvar(s.URL)
// check for restart
if s.Err != nil && err == nil {
s.Restarted = true
Expand Down Expand Up @@ -108,27 +106,6 @@ func guessValue(value *jason.Value) interface{} {
return nil
}

// Addr returns fully qualified host:port pair for service.
//
// If host is not specified, 'localhost' is used.
func (s Service) Addr() string {
if strings.HasPrefix(s.Port, "https://") {
return fmt.Sprintf("%s%s", s.Port, ExpvarsURL)
}
// Try as port only
_, err := strconv.Atoi(s.Port)
if err == nil {
return fmt.Sprintf("http://localhost:%s%s", s.Port, ExpvarsURL)
}

host, port, err := net.SplitHostPort(s.Port)
if err == nil {
return fmt.Sprintf("http://%s:%s%s", host, port, ExpvarsURL)
}

return ""
}

// Value returns current value for the given var of this service.
//
// It also formats value, if kind is specified.
Expand Down
2 changes: 1 addition & 1 deletion ui_dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (*DummyUI) Update(data UIData) {
for _, service := range data.Services {
fmt.Printf("%s: ", service.Name)
if service.Err != nil {
fmt.Printf("ERROR: %s", service.Err)
fmt.Printf("ERROR: %s\n", service.Err)
continue
}

Expand Down
98 changes: 72 additions & 26 deletions utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package main
import (
"errors"
"fmt"
"net"
"net/url"
"path/filepath"
"strings"

"github.com/bsiegert/ranges"
)

var ErrParsePorts = fmt.Errorf("cannot parse ports argument")

// ParseVars returns parsed and validated slice of strings with
// variables names that will be used for monitoring.
func ParseVars(vars string) ([]VarName, error) {
Expand All @@ -35,48 +37,83 @@ func BaseCommand(cmdline []string) string {
return filepath.Base(cmdline[0])
}

// ParsePorts converts comma-separated ports into strings slice
func ParsePorts(s string) ([]string, error) {
var (
ports []string
err error
)
// Try simple mode, ports only ("1234-1235,80")
ports, err = parseRange(s)
if err == nil {
return ports, nil
// flattenURLs returns URLs for the given addr and set of ports.
//
// Note, rawurl shouldn't contain port, as port will be appended.
func flattenURLs(rawurl string, ports []string) ([]url.URL, error) {
var urls []url.URL

// Add http by default
if !strings.HasPrefix(rawurl, "http") {
rawurl = fmt.Sprintf("http://%s", rawurl)
}

// Make URL from rawurl
baseUrl, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
baseUrl.Path = ExpvarsPath

var ErrParsePorts = fmt.Errorf("cannot parse ports argument")
// Create new URL for each port
for _, port := range ports {
u := *baseUrl
u.Host = fmt.Sprintf("%s:%s", u.Host, port)
urls = append(urls, u)
}
return urls, nil
}

// else, try host:ports notation ("localhost:1234-1235,remote:2000,2345")
// ParsePorts parses and flattens comma-separated ports/urls into URLs slice
func ParsePorts(s string) ([]url.URL, error) {
var urls []url.URL
fields := strings.FieldsFunc(s, func(r rune) bool { return r == ',' })
for _, field := range fields {
// split host:ports
var host, portsRange string
// Try simple 'ports range' mode, ports only ("1234-1235,80")
// Defaults to "localhost" will be used.
ports, err := parseRange(field)
if err == nil {
furls, err := flattenURLs("http://localhost", ports)
if err != nil {
return nil, err
}
urls = append(urls, furls...)
continue
}

// then, try host:ports notation ("localhost:1234-1235,https://remote:2000,2345")
var rawurl, portsRange string
parts := strings.FieldsFunc(field, func(r rune) bool { return r == ':' })
if len(parts) == 1 {
host = "localhost"
} else if len(parts) == 2 {
host, portsRange = parts[0], parts[1]
} else {
return nil, ErrParsePorts
switch len(parts) {
case 1:
// "1234-234"
rawurl = "http://localhost"
case 2:
// "localhost:1234"
rawurl, portsRange = parts[0], parts[1]
default:
// "https://user:[email protected]:1234"
rawurl = strings.Join(parts[:len(parts)-1], ":")
portsRange = parts[len(parts)-1]
}

pp, err := parseRange(portsRange)
ports, err = parseRange(portsRange)
if err != nil {
return nil, ErrParsePorts
}

for _, p := range pp {
addr := net.JoinHostPort(host, p)
ports = append(ports, addr)
purls, err := flattenURLs(rawurl, ports)
if err != nil {
return nil, ErrParsePorts
}

urls = append(urls, purls...)
}

return ports, nil
return urls, nil
}

// parseRange flattens port ranges, such as "1234-1240,1333"
func parseRange(s string) ([]string, error) {
portsInt, err := ranges.Parse(s)
if err != nil {
Expand All @@ -89,3 +126,12 @@ func parseRange(s string) ([]string, error) {
}
return ports, nil
}

// NewURL returns net.URL for the given port, with expvarmon defaults set.
func NewURL(port string) url.URL {
return url.URL{
Scheme: "http",
Host: fmt.Sprintf("localhost:%s"),
Path: "/debug/vars",
}
}
28 changes: 24 additions & 4 deletions utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestPorts(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(ports) != 2 || ports[0] != "1234" {
if len(ports) != 2 || ports[0].Host != "localhost:1234" {
t.Fatalf("ParsePorts returns wrong data: %v", ports)
}

Expand All @@ -41,16 +41,36 @@ func TestPorts(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(ports) != 5 || ports[0] != "1234" || ports[4] != "2000" {
if len(ports) != 5 || ports[0].Host != "localhost:1234" || ports[4].Host != "localhost:2000" {
t.Fatalf("ParsePorts returns wrong data: %v", ports)
}

arg = "localhost:2000-2002,remote:1234-1235"
arg = "40000-40002,localhost:2000-2002,remote:1234-1235,https://example.com:1234-1236"
ports, err = ParsePorts(arg)
if err != nil {
t.Fatal(err)
}
if len(ports) != 5 || ports[0] != "localhost:2000" || ports[4] != "remote:1235" {
if len(ports) != 11 ||
ports[0].Host != "localhost:40000" ||
ports[3].Host != "localhost:2000" ||
ports[7].Host != "remote:1235" ||
ports[7].Path != "/debug/vars" ||
ports[10].Host != "example.com:1236" ||
ports[10].Scheme != "https" {
t.Fatalf("ParsePorts returns wrong data: %v", ports)
}

// Test Auth
arg = "http://user:pass@localhost:2000-2002"
ports, err = ParsePorts(arg)
if err != nil {
t.Fatal(err)
}
pass, isSet := ports[0].User.Password()
if len(ports) != 3 ||
ports[0].User.Username() != "user" ||
pass != "pass" || !isSet ||
ports[0].Scheme != "http" {
t.Fatalf("ParsePorts returns wrong data: %v", ports)
}

Expand Down

0 comments on commit dff1519

Please sign in to comment.