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:
parent
60887e4540
commit
dff15195c1
10
expvars.go
10
expvars.go
@ -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)
|
||||
}
|
||||
|
9
main.go
9
main.go
@ -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()
|
||||
|
8
self.go
8
self.go
@ -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")
|
||||
}
|
||||
|
35
service.go
35
service.go
@ -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.
|
||||
|
@ -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
100
utils.go
@ -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",
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user