Internal tool.
commit
880ada1e4d
|
@ -0,0 +1,172 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
var (
|
||||
logPath string
|
||||
bindAddress string
|
||||
maxBackString string
|
||||
maxBack time.Duration
|
||||
|
||||
reLine = regexp.MustCompile(`([^ ]+) - - \[([^\]]+)\] "([^"]+)" ([0-9]+) ([0-9]+) "([^"]+)" "([^"]+)"`)
|
||||
)
|
||||
|
||||
type logEntry struct {
|
||||
address net.IP
|
||||
timestamp time.Time
|
||||
request string
|
||||
referrer string
|
||||
agent string
|
||||
}
|
||||
|
||||
func parseNginxLine(line []byte) (logEntry, error) {
|
||||
entry := logEntry{}
|
||||
matches := reLine.FindSubmatch(line)
|
||||
if matches == nil {
|
||||
return entry, errors.New("Could not parse line.")
|
||||
}
|
||||
entry.address = net.ParseIP(string(matches[1]))
|
||||
t, err := time.Parse("2/Jan/2006:15:04:05 -0700", string(matches[2]))
|
||||
if err != nil {
|
||||
return entry, err
|
||||
}
|
||||
entry.timestamp = t
|
||||
entry.request = string(matches[3])
|
||||
entry.referrer = string(matches[6])
|
||||
entry.agent = string(matches[7])
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
func getBackground(address net.IP) (string, error) {
|
||||
f, err := os.Open(logPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Seek to end of file.
|
||||
if _, err = f.Seek(0, os.SEEK_END); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = f.Seek(-1, os.SEEK_CUR); err != nil {
|
||||
return "", err
|
||||
}
|
||||
now := time.Now()
|
||||
for {
|
||||
// Seek backwards until next newline.
|
||||
line := []byte{}
|
||||
for {
|
||||
d := make([]byte, 1)
|
||||
if _, err = io.ReadFull(f, d); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = f.Seek(-2, os.SEEK_CUR); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if string(d) == "\n" {
|
||||
break
|
||||
}
|
||||
line = append(d, line...)
|
||||
}
|
||||
if string(line) == "" {
|
||||
continue
|
||||
}
|
||||
entry, err := parseNginxLine(line)
|
||||
if err != nil {
|
||||
glog.Errorf("Could not parse line %s: %v", line, err)
|
||||
continue
|
||||
}
|
||||
if now.Sub(entry.timestamp) > maxBack {
|
||||
break
|
||||
}
|
||||
if !entry.address.Equal(address) {
|
||||
continue
|
||||
}
|
||||
if entry.referrer == "-" {
|
||||
continue
|
||||
}
|
||||
u, err := url.Parse(entry.referrer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(u.Host, "google") || u.Path != "/search" {
|
||||
continue
|
||||
}
|
||||
glog.Infof("Entry: %s, %s", entry.address.String(), entry.referrer)
|
||||
query := u.Query().Get("q")
|
||||
if query != "" {
|
||||
t := humanize.Time(entry.timestamp)
|
||||
return fmt.Sprintf("Address %s searched %s for '%s' %s", address.String(), u.Host, query, t), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("No background.")
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:error`
|
||||
Data string `json:data`
|
||||
}
|
||||
|
||||
func handleCheck(w http.ResponseWriter, r *http.Request) {
|
||||
response := apiResponse{}
|
||||
defer func() {
|
||||
data, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
glog.Error("While marshalling response: %v", err)
|
||||
w.WriteHeader(500)
|
||||
w.Write([]byte("Internal server error."))
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write(data)
|
||||
}()
|
||||
addressString := r.URL.Query().Get("address")
|
||||
if addressString == "" {
|
||||
response.Error = "No address specified."
|
||||
return
|
||||
}
|
||||
address := net.ParseIP(addressString)
|
||||
if address == nil {
|
||||
response.Error = "Invalid address."
|
||||
return
|
||||
}
|
||||
|
||||
background, err := getBackground(address)
|
||||
if err != nil {
|
||||
response.Error = "No background."
|
||||
glog.Errorf("While getting background for %s: %v", addressString, err)
|
||||
return
|
||||
}
|
||||
response.Data = background
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&logPath, "nginx_log_path", "/var/log/nginx/access_log", "Path to NGINX access log.")
|
||||
flag.StringVar(&bindAddress, "bind_address", ":8080", "Port to bind HTTP/JSON interface to.")
|
||||
flag.StringVar(&maxBackString, "max_back", "5m", "How far back in time to look for records.")
|
||||
flag.Parse()
|
||||
|
||||
d, err := time.ParseDuration(maxBackString)
|
||||
if err != nil {
|
||||
glog.Exit(err)
|
||||
}
|
||||
maxBack = d
|
||||
|
||||
glog.Infof("Starting on %s, watching %s.", bindAddress, logPath)
|
||||
http.HandleFunc("/api/1/background", handleCheck)
|
||||
glog.Exit(http.ListenAndServe(bindAddress, nil))
|
||||
}
|
Reference in New Issue