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  }