167 lines
3.9 KiB
Go
167 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hpcloud/tail"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/satyrius/gonx"
|
|
)
|
|
|
|
type NginxExporter struct {
|
|
AccessLogPath string
|
|
|
|
responses *prometheus.SummaryVec
|
|
error_responses *prometheus.SummaryVec
|
|
}
|
|
|
|
func NewNginxExporter(accessLogPath string) (*NginxExporter, error) {
|
|
const subsystem = "http"
|
|
|
|
return &NginxExporter{
|
|
AccessLogPath: accessLogPath,
|
|
responses: prometheus.NewSummaryVec(
|
|
prometheus.SummaryOpts{
|
|
Namespace: Namespace,
|
|
Subsystem: subsystem,
|
|
Name: "responses",
|
|
Help: "Summary of HTTP responses",
|
|
},
|
|
[]string{"project", "network", "protocol"}),
|
|
error_responses: prometheus.NewSummaryVec(
|
|
prometheus.SummaryOpts{
|
|
Namespace: Namespace,
|
|
Subsystem: subsystem,
|
|
Name: "error_responses",
|
|
Help: "Summary of error HTTP responses",
|
|
},
|
|
[]string{"project", "network", "protocol"}),
|
|
}, nil
|
|
}
|
|
|
|
// Implements prometheus.Collector
|
|
func (e *NginxExporter) Describe(ch chan<- *prometheus.Desc) {
|
|
e.responses.Describe(ch)
|
|
e.error_responses.Describe(ch)
|
|
}
|
|
|
|
// Implements prometheus.Collector
|
|
func (e *NginxExporter) Collect(ch chan<- prometheus.Metric) {
|
|
e.responses.Collect(ch)
|
|
e.error_responses.Collect(ch)
|
|
}
|
|
|
|
var nginxProjectRe = regexp.MustCompile("(?i)^\\w+ /(?:pub/)?([^/?]+)/[^\\s]* HTTP")
|
|
|
|
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"`)
|
|
|
|
if reader == nil {
|
|
log.Printf("Failed to create reader for line: %s\n", line)
|
|
return
|
|
}
|
|
|
|
// 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
|
|
sourceIP, err := entry.Field("remote_addr")
|
|
network := "unknown"
|
|
protocol := "unknown"
|
|
|
|
if err == nil {
|
|
ip := net.ParseIP(sourceIP)
|
|
network = IdentifyNetwork(ip)
|
|
protocol = IdentifyIPProtocol(ip)
|
|
} else {
|
|
log.Println(err)
|
|
}
|
|
|
|
request, err := entry.Field("request")
|
|
project := "unknown"
|
|
|
|
if err == nil {
|
|
match := nginxProjectRe.FindStringSubmatch(request)
|
|
|
|
if len(match) > 1 && IsMirroredProject(match[1]) {
|
|
project = match[1]
|
|
} else {
|
|
if len(match) > 1 { log.Println(match[1], "not found") }
|
|
project = "none"
|
|
}
|
|
} else {
|
|
log.Println(err)
|
|
}
|
|
|
|
sizeStr, err := entry.Field("bytes_sent")
|
|
if err != nil {
|
|
sizeStr = "0"
|
|
}
|
|
size, _ := strconv.ParseFloat(sizeStr, 64)
|
|
|
|
statusStr, err := entry.Field("status")
|
|
success := false
|
|
|
|
if err == nil {
|
|
responseCode, _ := strconv.Atoi(statusStr)
|
|
|
|
if responseCode >= 100 && responseCode <= 399 {
|
|
success = true
|
|
}
|
|
}
|
|
|
|
// Setup labels
|
|
labels := prometheus.Labels{
|
|
"project": project,
|
|
"network": network,
|
|
"protocol": protocol,
|
|
}
|
|
|
|
// Increment totals
|
|
e.responses.With(labels).Observe(size)
|
|
|
|
if !success {
|
|
e.error_responses.With(labels).Observe(size)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|