1
0
mirror of https://github.com/divan/expvarmon.git synced 2025-04-25 13:48:54 +08:00

Refactored URLs handling

This commit is contained in:
Ivan Daniluk 2015-07-10 19:29:22 +03:00
parent 60887e4540
commit dff15195c1
7 changed files with 119 additions and 73 deletions

View File

@ -4,6 +4,7 @@ import (
"errors"
"io"
"net/http"
"net/url"
"os"
"time"
@ -11,7 +12,7 @@ import (
)
// 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 {
@ -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)
}

View File

@ -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()

View File

@ -5,6 +5,7 @@ import (
"fmt"
"net"
"net/http"
"net/url"
"runtime"
"time"
)
@ -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)
@ -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")
}

View File

@ -1,9 +1,7 @@
package main
import (
"fmt"
"net"
"strconv"
"net/url"
"strings"
"sync"
@ -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
@ -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,
}
@ -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
@ -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.

View File

@ -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
}

100
utils.go
View File

@ -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) {
@ -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)
}
var ErrParsePorts = fmt.Errorf("cannot parse ports argument")
// Make URL from rawurl
baseUrl, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
baseUrl.Path = ExpvarsPath
// else, try host:ports notation ("localhost:1234-1235,remote:2000,2345")
// 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
}
// 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
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
// 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
}
pp, err := parseRange(portsRange)
// 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 == ':' })
switch len(parts) {
case 1:
// "1234-234"
rawurl = "http://localhost"
case 2:
// "localhost:1234"
rawurl, portsRange = parts[0], parts[1]
default:
// "https://user:pass@remote.name:1234"
rawurl = strings.Join(parts[:len(parts)-1], ":")
portsRange = parts[len(parts)-1]
}
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 {
@ -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",
}
}

View File

@ -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)
}
@ -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)
}