github.com/honeycombio/honeytail@v1.9.0/parsers/nginx/nginx.go (about) 1 // Package nginx consumes nginx logs 2 package nginx 3 4 import ( 5 "errors" 6 "fmt" 7 "os" 8 "strconv" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/sirupsen/logrus" 14 "github.com/honeycombio/gonx" 15 16 "github.com/honeycombio/honeytail/event" 17 "github.com/honeycombio/honeytail/httime" 18 "github.com/honeycombio/honeytail/parsers" 19 ) 20 21 const ( 22 commonLogFormatTimeLayout = "02/Jan/2006:15:04:05 -0700" 23 iso8601TimeLayout = "2006-01-02T15:04:05-07:00" 24 ) 25 26 type Options struct { 27 ConfigFile string `long:"conf" description:"Path to Nginx config file"` 28 LogFormatName string `long:"format" description:"Log format name to look for in the Nginx config file"` 29 TimeFieldName string `long:"timefield" description:"Name of the field that contains a timestamp"` 30 TimeFieldFormat string `long:"time_format" description:"Timestamp format to use (strftime and Golang time.Parse supported)"` 31 32 NumParsers int `hidden:"true" description:"number of nginx parsers to spin up"` 33 } 34 35 type Parser struct { 36 conf Options 37 lineParser parsers.LineParser 38 } 39 40 func (n *Parser) Init(options interface{}) error { 41 n.conf = *options.(*Options) 42 43 if n.conf.ConfigFile == "" { 44 return errors.New("missing required option --nginx.conf=<path to your Nginx config file>") 45 } 46 47 // Verify we've got our config, find our format 48 nginxConfig, err := os.Open(n.conf.ConfigFile) 49 if err != nil { 50 return fmt.Errorf("couldn't open Nginx config file %s: %v", n.conf.ConfigFile, err) 51 } 52 defer nginxConfig.Close() 53 // get the nginx log format from the config file 54 // get a nginx log parser 55 parser, err := gonx.NewNginxParser(nginxConfig, n.conf.LogFormatName) 56 if err != nil { 57 return err 58 } 59 gonxParser := &GonxLineParser{ 60 parser: parser, 61 } 62 n.lineParser = gonxParser 63 return nil 64 } 65 66 type GonxLineParser struct { 67 parser *gonx.Parser 68 } 69 70 func (g *GonxLineParser) ParseLine(line string) (map[string]interface{}, error) { 71 gonxEvent, err := g.parser.ParseString(line) 72 if err != nil { 73 logrus.WithFields(logrus.Fields{ 74 "logline": line, 75 "error": err, 76 }).Debug("failed to parse nginx log line") 77 return nil, err 78 } 79 return typeifyParsedLine(gonxEvent.Fields), nil 80 } 81 82 func (n *Parser) ProcessLines(lines <-chan string, send chan<- event.Event, prefixRegex *parsers.ExtRegexp) { 83 // parse lines one by one 84 wg := sync.WaitGroup{} 85 for i := 0; i < n.conf.NumParsers; i++ { 86 wg.Add(1) 87 go func() { 88 for line := range lines { 89 line = strings.TrimSpace(line) 90 logrus.WithFields(logrus.Fields{ 91 "line": line, 92 }).Debug("Attempting to process nginx log line") 93 94 // take care of any headers on the line 95 var prefixFields map[string]interface{} 96 if prefixRegex != nil { 97 var prefix string 98 prefix, fields := prefixRegex.FindStringSubmatchMap(line) 99 line = strings.TrimPrefix(line, prefix) 100 prefixFields = typeifyParsedLine(fields) 101 } 102 103 parsedLine, err := n.lineParser.ParseLine(line) 104 if err != nil { 105 continue 106 } 107 // merge the prefix fields and the parsed line contents 108 for k, v := range prefixFields { 109 parsedLine[k] = v 110 } 111 timestamp := n.getTimestamp(parsedLine) 112 113 e := event.Event{ 114 Timestamp: timestamp, 115 Data: parsedLine, 116 } 117 send <- e 118 } 119 wg.Done() 120 }() 121 } 122 wg.Wait() 123 logrus.Debug("lines channel is closed, ending nginx processor") 124 } 125 126 // typeifyParsedLine attempts to cast numbers in the event to floats or ints 127 func typeifyParsedLine(pl map[string]string) map[string]interface{} { 128 // try to convert numbers, if possible 129 msi := make(map[string]interface{}, len(pl)) 130 for k, v := range pl { 131 switch { 132 case strings.Contains(v, "."): 133 f, err := strconv.ParseFloat(v, 64) 134 if err == nil { 135 msi[k] = f 136 continue 137 } 138 case v == "-": 139 // no value, don't set a "-" string 140 continue 141 default: 142 i, err := strconv.ParseInt(v, 10, 64) 143 if err == nil { 144 msi[k] = i 145 continue 146 } 147 } 148 msi[k] = v 149 } 150 return msi 151 } 152 153 // tries to extract a timestamp from the log line 154 func (n *Parser) getTimestamp(evMap map[string]interface{}) time.Time { 155 var ( 156 setBothFieldsMsg = "Timestamp format and field must both be set to be used, one was not. Using current time instead." 157 ) 158 159 // Custom (user-defined) timestamp field/format takes priority over the 160 // default parsing behavior. Try that first. 161 if n.conf.TimeFieldFormat != "" || n.conf.TimeFieldName != "" { 162 if n.conf.TimeFieldFormat == "" || n.conf.TimeFieldName == "" { 163 logrus.Debug(setBothFieldsMsg) 164 return httime.Now() 165 } 166 return httime.GetTimestamp(evMap, n.conf.TimeFieldName, n.conf.TimeFieldFormat) 167 } 168 169 if _, ok := evMap["time_local"]; ok { 170 return httime.GetTimestamp(evMap, "time_local", commonLogFormatTimeLayout) 171 } 172 173 if _, ok := evMap["time_iso8601"]; ok { 174 return httime.GetTimestamp(evMap, "time_iso8601", iso8601TimeLayout) 175 } 176 177 if _, ok := evMap["msec"]; ok { 178 return httime.GetTimestamp(evMap, "msec", "") 179 } 180 181 return httime.GetTimestamp(evMap, "", "") 182 }