Browse Source

Rename to mirror_exporter; change format to proper exporter

master
Zachary Seguin 5 years ago
parent
commit
6dde4a2a96
  1. 2
      .gitignore
  2. 47
      mirror_exporter.go
  3. 117
      mirrorstats.go
  4. 30
      networks.go
  5. 220
      nginx.go
  6. 30
      request.go

2
.gitignore vendored

@ -1 +1 @@
mirrorstats
mirror_exporter

47
mirror_exporter.go

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

117
mirrorstats.go

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

30
networks.go

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

220
nginx.go

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

30
request.go

@ -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…
Cancel
Save