6 changed files with 249 additions and 197 deletions
@ -0,0 +1,47 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"log" |
||||
"net/http" |
||||
|
||||
"github.com/prometheus/client_golang/prometheus" |
||||
) |
||||
|
||||
const Namespace = "mirror" |
||||
|
||||
func main() { |
||||
// Define command line flags
|
||||
var ( |
||||
listenAddress = flag.String("net.listen-address", ":2400", "Address to listen on for web and metrics interface") |
||||
metricsPath = flag.String("net.telemetry-path", "/metrics", "Path to expose metrics on") |
||||
) |
||||
|
||||
flag.Parse() |
||||
|
||||
nginxExporter, err := NewNginxExporter("/var/log/nginx/access.log") |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
prometheus.MustRegister(nginxExporter) |
||||
|
||||
go nginxExporter.Monitor() |
||||
|
||||
http.Handle(*metricsPath, prometheus.Handler()) |
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
||||
w.Write([]byte(`<!doctype html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<title>Mirror Exporter</title> |
||||
</head> |
||||
<body> |
||||
<h1>Mirror Exporter</h1> |
||||
<p><a href="` + *metricsPath + `">Metrics</a></p> |
||||
</body> |
||||
</html>`)) |
||||
}) |
||||
|
||||
log.Printf("Exporter started.. Listening on %s, metrics available at %s", *listenAddress, *metricsPath) |
||||
log.Fatal(http.ListenAndServe(*listenAddress, nil)) |
||||
} |
@ -1,117 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"log" |
||||
"net" |
||||
"flag" |
||||
"net/http" |
||||
"regexp" |
||||
|
||||
"github.com/prometheus/client_golang/prometheus" |
||||
"github.com/prometheus/client_golang/prometheus/promhttp" |
||||
) |
||||
|
||||
var ( |
||||
CampusNets = [...]net.IPNet{ |
||||
net.IPNet{ net.ParseIP("10.0.0.0"), net.CIDRMask(8, 32), }, |
||||
net.IPNet{ net.ParseIP("129.97.0.0"), net.CIDRMask(16, 32), }, |
||||
net.IPNet{ net.ParseIP("172.16.0.0"), net.CIDRMask(12, 32), }, |
||||
net.IPNet{ net.ParseIP("192.168.0.0"), net.CIDRMask(16, 32), }, |
||||
net.IPNet{ net.ParseIP("2620:101:f000::"), net.CIDRMask(47, 128), }, |
||||
net.IPNet{ net.ParseIP("fd74:3ae8:4dde::"), net.CIDRMask(47, 128), }, |
||||
} |
||||
|
||||
requestsCounter = prometheus.NewCounterVec( |
||||
prometheus.CounterOpts{ |
||||
Namespace: "mirror", |
||||
Name: "requests", |
||||
Help: "Number of requests", |
||||
}, |
||||
[]string{"project", "network", "protocol", "success"}, |
||||
) |
||||
|
||||
bytesCounter = prometheus.NewCounterVec( |
||||
prometheus.CounterOpts{ |
||||
Namespace: "mirror", |
||||
Name: "bytes", |
||||
Help: "Number of bytes transferred", |
||||
}, |
||||
[]string{"project", "network", "protocol", "success"}, |
||||
) |
||||
) |
||||
|
||||
func init() { |
||||
prometheus.MustRegister(requestsCounter) |
||||
prometheus.MustRegister(bytesCounter) |
||||
} |
||||
|
||||
func networksContainIP(ip net.IP, networks []net.IPNet) bool { |
||||
for _, net := range networks { |
||||
if net.Contains(ip) { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
var addr = flag.String("web.listen", ":2400", "Address on which to exposer metrics and web interface.") |
||||
var nginxLogPath = flag.String("nginx.access-log", "/var/log/nginx/access.log", "Path to the nginx access log.") |
||||
|
||||
func boolToString(b bool) string { |
||||
if b { |
||||
return "yes" |
||||
} else { |
||||
return "no" |
||||
} |
||||
} |
||||
|
||||
func process(requests <-chan *Request) { |
||||
re := regexp.MustCompile("(?i)^/(?:pub/)?([^/]+)/") |
||||
|
||||
for request := range requests { |
||||
projectMatch := re.FindStringSubmatch(request.Path) |
||||
|
||||
// Ignore non-project pages
|
||||
if len(projectMatch) != 2 { |
||||
continue |
||||
} |
||||
project := projectMatch[1] |
||||
|
||||
// Determine network
|
||||
net := "other" |
||||
|
||||
if networksContainIP(request.SourceIP, CampusNets[:]) { |
||||
net = "uwaterloo" |
||||
} |
||||
|
||||
labels := prometheus.Labels{ |
||||
"project": project, |
||||
"network": net, |
||||
"protocol": request.Protocol.String(), |
||||
"success": boolToString(request.Success), |
||||
} |
||||
requestsCounter.With(labels).Inc() |
||||
bytesCounter.With(labels).Add(request.Size) |
||||
} |
||||
} |
||||
|
||||
func main() { |
||||
flag.Parse() |
||||
http.Handle("/metrics", promhttp.Handler()) |
||||
|
||||
nginxCollector, err := NginxCollect(*nginxLogPath) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
// Wait chan to pass requests back
|
||||
requests := make(chan *Request, 100) |
||||
|
||||
// Start collectors
|
||||
go nginxCollector.Collect(requests) |
||||
|
||||
// Start request process
|
||||
go process(requests) |
||||
log.Fatal(http.ListenAndServe(*addr, nil)) |
||||
} |
@ -0,0 +1,30 @@
|
||||
package main |
||||
|
||||
import( |
||||
"net" |
||||
) |
||||
|
||||
var ( |
||||
Networks = map[string][]net.IPNet{ |
||||
"uwaterloo": []net.IPNet{ |
||||
net.IPNet{ net.ParseIP("10.0.0.0"), net.CIDRMask(8, 32), }, |
||||
net.IPNet{ net.ParseIP("129.97.0.0"), net.CIDRMask(16, 32), }, |
||||
net.IPNet{ net.ParseIP("172.16.0.0"), net.CIDRMask(12, 32), }, |
||||
net.IPNet{ net.ParseIP("192.168.0.0"), net.CIDRMask(16, 32), }, |
||||
net.IPNet{ net.ParseIP("2620:101:f000::"), net.CIDRMask(47, 128), }, |
||||
net.IPNet{ net.ParseIP("fd74:3ae8:4dde::"), net.CIDRMask(47, 128), }, |
||||
}, |
||||
} |
||||
) |
||||
|
||||
func IdentifyNetwork(ip net.IP) string { |
||||
for name, nets := range Networks { |
||||
for _, net := range nets { |
||||
if net.Contains(ip) { |
||||
return name |
||||
} |
||||
} |
||||
} |
||||
|
||||
return "other" |
||||
} |
@ -1,86 +1,208 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"os" |
||||
"strings" |
||||
"log" |
||||
"net" |
||||
"strconv" |
||||
"os" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/hpcloud/tail" |
||||
"github.com/prometheus/client_golang/prometheus" |
||||
"github.com/satyrius/gonx" |
||||
) |
||||
|
||||
type NginxCollector struct { |
||||
tail *tail.Tail |
||||
} |
||||
type NginxExporter struct { |
||||
AccessLogPath string |
||||
|
||||
func NginxCollect(file string) (*NginxCollector, error) { |
||||
// Create the tail
|
||||
tail, err := tail.TailFile(file, tail.Config{ |
||||
Follow: true, |
||||
ReOpen: true, |
||||
Poll: true, |
||||
Location: &tail.SeekInfo{ |
||||
Offset: 0, |
||||
Whence: os.SEEK_END, |
||||
}, |
||||
}); |
||||
// Request counts
|
||||
responses *prometheus.CounterVec |
||||
error_responses *prometheus.CounterVec |
||||
|
||||
// Bytes count
|
||||
bytes_sent *prometheus.CounterVec |
||||
error_bytes_sent *prometheus.CounterVec |
||||
} |
||||
|
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
func NewNginxExporter(accessLogPath string) (*NginxExporter, error) { |
||||
const subsystem = "http" |
||||
|
||||
return &NginxExporter{ |
||||
AccessLogPath: accessLogPath, |
||||
responses: prometheus.NewCounterVec( |
||||
prometheus.CounterOpts{ |
||||
Namespace: Namespace, |
||||
Subsystem: subsystem, |
||||
Name: "responses_total", |
||||
Help: "Number of HTTP responses", |
||||
}, |
||||
[]string{"project", "network", "protocol", "useragent"}, |
||||
), |
||||
error_responses: prometheus.NewCounterVec( |
||||
prometheus.CounterOpts{ |
||||
Namespace: Namespace, |
||||
Subsystem: subsystem, |
||||
Name: "error_responses_total", |
||||
Help: "Number of HTTP error responses (HTTP response code not between 100 - 399)", |
||||
}, |
||||
[]string{"project", "network", "protocol", "useragent"}, |
||||
), |
||||
bytes_sent: prometheus.NewCounterVec( |
||||
prometheus.CounterOpts{ |
||||
Namespace: Namespace, |
||||
Subsystem: subsystem, |
||||
Name: "response_sent_bytes", |
||||
Help: "Number of bytes sent in HTTP responses", |
||||
}, |
||||
[]string{"project", "network", "protocol", "useragent"}, |
||||
), |
||||
error_bytes_sent: prometheus.NewCounterVec( |
||||
prometheus.CounterOpts{ |
||||
Namespace: Namespace, |
||||
Subsystem: subsystem, |
||||
Name: "error_response_sent_bytes", |
||||
Help: "Number of bytes sent in error HTTP responses (HTTP response code not between 100 - 399)", |
||||
}, |
||||
[]string{"project", "network", "protocol", "useragent"}, |
||||
), |
||||
}, nil |
||||
} |
||||
|
||||
collector := &NginxCollector{ |
||||
tail: tail, |
||||
} |
||||
return collector, nil |
||||
// Implements prometheus.Collector
|
||||
func (e *NginxExporter) Describe(ch chan<- *prometheus.Desc) { |
||||
e.responses.Describe(ch) |
||||
e.error_responses.Describe(ch) |
||||
e.bytes_sent.Describe(ch) |
||||
e.error_bytes_sent.Describe(ch) |
||||
} |
||||
|
||||
// Implements prometheus.Collector
|
||||
func (e *NginxExporter) Collect(ch chan<- prometheus.Metric) { |
||||
e.responses.Collect(ch) |
||||
e.error_responses.Collect(ch) |
||||
e.bytes_sent.Collect(ch) |
||||
e.error_bytes_sent.Collect(ch) |
||||
} |
||||
|
||||
var pathRe = regexp.MustCompile("(?i)\\w+ ([^\\s]+)") |
||||
var nginxProjectRe = regexp.MustCompile("(?i)^\\w+ /([^/]+)/[^\\s]* HTTP") |
||||
|
||||
func processNginxLogLine(line *tail.Line, c chan<- *Request) { |
||||
lineReader := strings.NewReader(line.Text) |
||||
reader := gonx.NewReader(lineReader, `$remote_addr - $remote_user [$time_local] "$request" $status $bytes_sent "$http_referer" "$http_user_agent"`) |
||||
func (e *NginxExporter) processLogLine(line string) { |
||||
lineReader := strings.NewReader(line) |
||||
reader := gonx.NewReader( |
||||
lineReader, |
||||
`$remote_addr - $remote_user [$time_local] "$request" $status $bytes_sent "$http_referer" "$http_user_agent"`) |
||||
|
||||
// Don't continue if we failed to make the reader
|
||||
if reader == nil { |
||||
log.Printf("Failed to create reader for line: %s\n", line) |
||||
return |
||||
} |
||||
|
||||
// Read the entry
|
||||
// Read the entry on the line
|
||||
entry, err := reader.Read() |
||||
if err != nil { |
||||
log.Printf("Failed to get entry on line: %s\n", line) |
||||
return |
||||
} |
||||
|
||||
// Process information from the log line
|
||||
sourceIP, _ := entry.Field("remote_addr") |
||||
pathStr, _ := entry.Field("request") |
||||
statusStr, _ := entry.Field("status") |
||||
status, _ := strconv.ParseInt(statusStr, 10, 16) |
||||
sizeStr, _ := entry.Field("bytes_sent") |
||||
// Process
|
||||
sourceIP, err := entry.Field("remote_addr") |
||||
network := "unknown" |
||||
protocol := "unknown" |
||||
|
||||
if err == nil { |
||||
ip := net.ParseIP(sourceIP) |
||||
network = IdentifyNetwork(ip) |
||||
|
||||
if ip.To4() != nil { |
||||
protocol = "ipv4" |
||||
} else if ip.To16() != nil { |
||||
protocol = "ipv6" |
||||
} |
||||
} else { |
||||
log.Println(err) |
||||
} |
||||
|
||||
request, err := entry.Field("request") |
||||
project := "unknown" |
||||
|
||||
if err == nil { |
||||
match := nginxProjectRe.FindStringSubmatch(request) |
||||
|
||||
if len(match) > 1 { |
||||
project = match[1] |
||||
} else { |
||||
project = "none" |
||||
} |
||||
} else { |
||||
log.Println(err) |
||||
} |
||||
|
||||
userAgent, err := entry.Field("http_user_agent") |
||||
if err != nil { |
||||
log.Println(err) |
||||
userAgent = "unknown" |
||||
} |
||||
|
||||
sizeStr, err := entry.Field("bytes_sent") |
||||
if err != nil { |
||||
sizeStr = "0" |
||||
} |
||||
size, _ := strconv.ParseFloat(sizeStr, 64) |
||||
|
||||
path := pathRe.FindStringSubmatch(pathStr) |
||||
if len(path) != 2 { |
||||
return; |
||||
statusStr, err := entry.Field("status") |
||||
success := false |
||||
|
||||
if err == nil { |
||||
responseCode, _ := strconv.Atoi(statusStr) |
||||
|
||||
if responseCode >= 100 && responseCode <= 399 { |
||||
success = true |
||||
} |
||||
} |
||||
|
||||
request := Request{ |
||||
SourceIP: net.ParseIP(sourceIP), |
||||
Path: path[1], |
||||
Protocol: ProtocolHTTP, |
||||
Size: size, |
||||
Success: status >= 200 && status < 300, |
||||
// Setup labels
|
||||
labels := prometheus.Labels{ |
||||
"project": project, |
||||
"network": network, |
||||
"protocol": protocol, |
||||
"useragent": userAgent, |
||||
} |
||||
|
||||
c <- &request |
||||
// Increment totals
|
||||
e.responses.With(labels).Inc() |
||||
e.bytes_sent.With(labels).Add(size) |
||||
|
||||
if !success { |
||||
e.error_responses.With(labels).Inc() |
||||
e.error_bytes_sent.With(labels).Add(size) |
||||
} |
||||
} |
||||
|
||||
func (n *NginxCollector) Collect(c chan<- *Request) { |
||||
for line := range n.tail.Lines { |
||||
go processNginxLogLine(line, c) |
||||
func (e *NginxExporter) Monitor() { |
||||
tailConfig := tail.Config{ |
||||
Follow: true, |
||||
ReOpen: true, |
||||
Poll: true, |
||||
Location: &tail.SeekInfo{ |
||||
Offset: 0, |
||||
Whence: os.SEEK_END, |
||||
}, |
||||
} |
||||
t, err := tail.TailFile(e.AccessLogPath, tailConfig) |
||||
|
||||
// Continue to retry every 5 seconds
|
||||
for ; err != nil; { |
||||
t, err = tail.TailFile(e.AccessLogPath, tailConfig) |
||||
|
||||
if err != nil { |
||||
time.Sleep(time.Second * 5) |
||||
log.Println("Error starting nginx tail (retrying in 5 seconds)", err) |
||||
} |
||||
} |
||||
|
||||
for line := range t.Lines { |
||||
go e.processLogLine(line.Text) |
||||
} |
||||
} |
||||
|
@ -1,30 +0,0 @@
|
||||
package main |
||||
|
||||
import ( |
||||
"net" |
||||
) |
||||
|
||||
type RequestProtocol int |
||||
const ( |
||||
ProtocolHTTP RequestProtocol = iota |
||||
) |
||||
|
||||
func (p RequestProtocol) String() string { |
||||
switch(p) { |
||||
case ProtocolHTTP: |
||||
return "http" |
||||
} |
||||
|
||||
return "unknown" |
||||
} |
||||
|
||||
type Request struct { |
||||
// Client information
|
||||
SourceIP net.IP |
||||
Path string |
||||
Protocol RequestProtocol |
||||
|
||||
// Result
|
||||
Size float64 |
||||
Success bool |
||||
} |
Loading…
Reference in new issue