-
Notifications
You must be signed in to change notification settings - Fork 3
/
nginx.go
206 lines (176 loc) · 5.08 KB
/
nginx.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package main
import (
"bufio"
"errors"
"io"
"net"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/COSI-Lab/datarithms"
"github.com/COSI-Lab/logging"
"github.com/IncSW/geoip2"
"github.com/nxadm/tail"
)
// It is critical that NGINX uses the following log format:
/*
* log_format config '"$time_local" "$remote_addr" "$request" "$status" "$body_bytes_sent" "$request_length" "$http_user_agent"';
* access_log /var/log/nginx/access.log config;
*/
// NginxLogEntry is a struct that represents a parsed nginx log entry
type NginxLogEntry struct {
IP net.IP
City *geoip2.CityResult
Time time.Time
Method string
Distro string
Url string
Version string
Status int
BytesSent int64
BytesRecv int64
Agent string
}
var reQuotes = regexp.MustCompile(`"(.*?)"`)
// ReadNginxLogFile is a testing function that simulates tailing a log file by reading it line by line with some delay between lines
func ReadNginxLogFile(logFile string, channels ...chan *NginxLogEntry) (err error) {
for {
f, err := os.Open(logFile)
if err != nil {
return err
}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
entry, err := parseNginxLine(scanner.Text())
if err == nil {
// Send a pointer to the entry down each channel
for ch := range channels {
channels[ch] <- entry
}
}
time.Sleep(100 * time.Millisecond)
}
f.Close()
}
}
// TailNginxLogFile tails a log file and sends the parsed log entries to the specified channels
func TailNginxLogFile(logFile string, lastUpdated time.Time, channels ...chan *NginxLogEntry) {
// Find the offset of the line where the date is past lastUpdated
start := time.Now()
offset, err := datarithms.BinarySearchFileByDate(logFile, lastUpdated, parseNginxDate)
if err != nil {
logging.Error(err)
return
}
logging.Info("Found nginx log offset in", time.Since(start))
// Tail the log file `tail -F` starting at the offset
seek := tail.SeekInfo{
Offset: offset,
Whence: io.SeekStart,
}
tail, err := tail.TailFile(logFile, tail.Config{Follow: true, ReOpen: true, MustExist: true, Location: &seek})
if err != nil {
logging.Error("Failed to start tailing `nginx.log`:", err)
return
}
logging.Success("Tailing nginx log file")
// Parse each line as we receive it
for line := range tail.Lines {
entry, err := parseNginxLine(line.Text)
if err == nil {
// Send a pointer to the entry down each channel
for ch := range channels {
channels[ch] <- entry
}
}
}
}
// parseNginxDate parses a single line of the nginx log file and returns the time.Time of the line
func parseNginxDate(line string) (time.Time, error) {
tm, err := time.Parse("\"02/Jan/2006:15:04:05 -0700\"", reQuotes.FindString(line))
if err != nil {
return time.Time{}, err
}
return tm, nil
}
// parseNginxLine parses a single line of the nginx log file
// It's critical the log file uses the correct format found at the top of this file
// If the log file is not in the correct format or if some other part of the parsing fails
// this function will return an error
func parseNginxLine(line string) (*NginxLogEntry, error) {
// "$time_local" "$remote_addr" "$request" "$status" "$body_bytes_sent" "$request_length" "$http_user_agent";
quoteList := reQuotes.FindAllString(line, -1)
if len(quoteList) != 7 {
return nil, errors.New("invalid number of parameters in log entry")
}
// Trim quotation marks
for i := 0; i < len(quoteList); i++ {
quoteList[i] = quoteList[i][1 : len(quoteList[i])-1]
}
var entry NginxLogEntry
var err error
// Time
t := "02/Jan/2006:15:04:05 -0700"
tm, err := time.Parse(t, quoteList[0])
if err != nil {
return nil, err
}
entry.Time = tm
// IPv4 or IPv6 address
entry.IP = net.ParseIP(quoteList[1])
if entry.IP == nil {
return nil, errors.New("failed to parse ip")
}
// Optional GeoIP lookup
if geoipHandler != nil {
city, err := geoipHandler.Lookup(entry.IP)
if err != nil {
entry.City = nil
} else {
entry.City = city
}
} else {
entry.City = nil
}
// Method url http version
split := strings.Split(quoteList[2], " ")
if len(split) != 3 {
// this should never fail
return nil, errors.New("invalid number of strings in request")
}
entry.Method = split[0]
entry.Url = split[1]
entry.Version = split[2]
// Distro is the top level of the URL path
split = strings.Split(entry.Url, "/")
if len(split) >= 2 {
entry.Distro = split[1]
} else {
return nil, errors.New("invalid number of parts in url")
}
// HTTP response status
status, err := strconv.Atoi(quoteList[3])
if err != nil {
// this should never fail
return nil, errors.New("could not parse http response status")
}
entry.Status = status
// Bytes sent int64
bytesSent, err := strconv.ParseInt(quoteList[4], 10, 64)
if err != nil {
// this should never fail
return nil, errors.New("could not parse bytes_sent")
}
entry.BytesSent = bytesSent
// Bytes received
bytesRecv, err := strconv.ParseInt(quoteList[5], 10, 64)
if err != nil {
return nil, errors.New("could not parse bytes_recv")
}
entry.BytesRecv = bytesRecv
// User agent
entry.Agent = quoteList[6]
return &entry, nil
}