github.com/mackerelio/mackerel-agent-plugins@v0.89.3/mackerel-plugin-accesslog/lib/accesslog.go (about)

     1  package mpaccesslog
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"math"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"time"
    15  
    16  	"github.com/Songmu/axslogparser"
    17  	"github.com/Songmu/postailer"
    18  	mp "github.com/mackerelio/go-mackerel-plugin"
    19  	"github.com/mackerelio/golib/pluginutil"
    20  	"github.com/montanaflynn/stats"
    21  	"golang.org/x/text/cases"
    22  	"golang.org/x/text/language"
    23  )
    24  
    25  var parsers = axslogparser.Parsers{
    26  	Apache: &axslogparser.Apache{Loose: true},
    27  	LTSV:   &axslogparser.LTSV{Loose: true},
    28  }
    29  
    30  // AccesslogPlugin mackerel plugin
    31  type AccesslogPlugin struct {
    32  	prefix    string
    33  	file      string
    34  	posFile   string
    35  	parser    axslogparser.Parser
    36  	noPosFile bool
    37  }
    38  
    39  // MetricKeyPrefix interface for PluginWithPrefix
    40  func (p *AccesslogPlugin) MetricKeyPrefix() string {
    41  	if p.prefix == "" {
    42  		p.prefix = "accesslog"
    43  	}
    44  	return p.prefix
    45  }
    46  
    47  // GraphDefinition interface for mackerelplugin
    48  func (p *AccesslogPlugin) GraphDefinition() map[string]mp.Graphs {
    49  	labelPrefix := cases.Title(language.Und, cases.NoLower).String(p.prefix)
    50  	return map[string]mp.Graphs{
    51  		"access_num": {
    52  			Label: labelPrefix + " Access Num",
    53  			Unit:  "integer",
    54  			Metrics: []mp.Metrics{
    55  				{Name: "total_count", Label: "Total Count"},
    56  				{Name: "5xx_count", Label: "HTTP 5xx Count", Stacked: true},
    57  				{Name: "4xx_count", Label: "HTTP 4xx Count", Stacked: true},
    58  				{Name: "3xx_count", Label: "HTTP 3xx Count", Stacked: true},
    59  				{Name: "2xx_count", Label: "HTTP 2xx Count", Stacked: true},
    60  			},
    61  		},
    62  		"access_rate": {
    63  			Label: labelPrefix + " Access Rate",
    64  			Unit:  "percentage",
    65  			Metrics: []mp.Metrics{
    66  				{Name: "5xx_percentage", Label: "HTTP 5xx Percentage", Stacked: true},
    67  				{Name: "4xx_percentage", Label: "HTTP 4xx Percentage", Stacked: true},
    68  				{Name: "3xx_percentage", Label: "HTTP 3xx Percentage", Stacked: true},
    69  				{Name: "2xx_percentage", Label: "HTTP 2xx Percentage", Stacked: true},
    70  			},
    71  		},
    72  		"latency": {
    73  			Label: labelPrefix + " Latency",
    74  			Unit:  "float",
    75  			Metrics: []mp.Metrics{
    76  				{Name: "99_percentile", Label: "99 Percentile"},
    77  				{Name: "95_percentile", Label: "95 Percentile"},
    78  				{Name: "90_percentile", Label: "90 Percentile"},
    79  				{Name: "average", Label: "Average"},
    80  			},
    81  		},
    82  	}
    83  }
    84  
    85  var posRe = regexp.MustCompile(`^([a-zA-Z]):[/\\]`)
    86  
    87  func (p *AccesslogPlugin) getPosPath() string {
    88  	base := p.file + ".pos.json"
    89  	if p.posFile != "" {
    90  		if filepath.IsAbs(p.posFile) {
    91  			return p.posFile
    92  		}
    93  		base = p.posFile
    94  	}
    95  	return filepath.Join(
    96  		pluginutil.PluginWorkDir(),
    97  		"mackerel-plugin-accesslog.d",
    98  		posRe.ReplaceAllString(base, `$1`+string(filepath.Separator)),
    99  	)
   100  }
   101  
   102  // We'd like to better to use io.ReadSeekCloser interface if we can drop support for Go 1.15.
   103  type readSeekCloser interface {
   104  	io.Reader
   105  	io.Seeker
   106  	io.Closer
   107  }
   108  
   109  func (p *AccesslogPlugin) getReadSeekCloser() (readSeekCloser, bool, error) {
   110  	if p.noPosFile {
   111  		f, err := os.Open(p.file)
   112  		return f, true, err
   113  	}
   114  	posfile := p.getPosPath()
   115  	fi, err := os.Stat(posfile)
   116  	// don't output any metrics when the pos file doesn't exist or is too old
   117  	takeMetrics := err == nil && fi.ModTime().After(time.Now().Add(-2*time.Minute))
   118  	f, err := postailer.Open(p.file, posfile)
   119  	return f, takeMetrics, err
   120  }
   121  
   122  // FetchMetrics interface for mackerelplugin
   123  func (p *AccesslogPlugin) FetchMetrics() (map[string]float64, error) {
   124  	f, takeMetrics, err := p.getReadSeekCloser()
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  	defer f.Close()
   129  
   130  	if !takeMetrics {
   131  		// discard existing contents to seek position
   132  		_, err := f.Seek(0, io.SeekEnd)
   133  		return map[string]float64{}, err
   134  	}
   135  
   136  	countMetrics := []string{"total_count", "2xx_count", "3xx_count", "4xx_count", "5xx_count"}
   137  	ret := make(map[string]float64)
   138  	for _, k := range countMetrics {
   139  		ret[k] = 0
   140  	}
   141  	var reqtimes []float64
   142  	r := bufio.NewReader(f)
   143  	for {
   144  		var (
   145  			l   *axslogparser.Log
   146  			err error
   147  			bb  bytes.Buffer
   148  		)
   149  		buf, isPrefix, err := r.ReadLine()
   150  		bb.Write(buf)
   151  		for isPrefix {
   152  			buf, isPrefix, err = r.ReadLine()
   153  			if err != nil {
   154  				break
   155  			}
   156  			bb.Write(buf)
   157  		}
   158  		if err != nil {
   159  			if err != io.EOF {
   160  				log.Println(err)
   161  			}
   162  			break
   163  		}
   164  		line := bb.String()
   165  		if p.parser == nil {
   166  			p.parser, l, err = parsers.GuessParser(line)
   167  		} else {
   168  			l, err = p.parser.Parse(line)
   169  		}
   170  		if err != nil {
   171  			log.Println(err)
   172  			continue
   173  		}
   174  
   175  		// ignore invalid logs
   176  		if l.Status < 100 || 600 <= l.Status {
   177  			log.Printf("invalid log ignored (invalid status: %d)\n", l.Status)
   178  			continue
   179  		}
   180  
   181  		ret[string(fmt.Sprintf("%d", l.Status)[0])+"xx_count"]++
   182  		ret["total_count"]++
   183  
   184  		if l.ReqTimeMicroSec != nil {
   185  			reqtimes = append(reqtimes, *l.ReqTimeMicroSec*math.Pow(10, -6))
   186  		} else if l.ReqTime != nil {
   187  			reqtimes = append(reqtimes, *l.ReqTime)
   188  		} else if l.TakenSec != nil {
   189  			reqtimes = append(reqtimes, *l.TakenSec)
   190  		}
   191  	}
   192  	if ret["total_count"] > 0 {
   193  		for _, v := range []string{"2xx", "3xx", "4xx", "5xx"} {
   194  			ret[v+"_percentage"] = ret[v+"_count"] * 100 / ret["total_count"]
   195  		}
   196  	}
   197  	if len(reqtimes) > 0 {
   198  		ret["average"], _ = stats.Mean(reqtimes)
   199  		for _, v := range []int{90, 95, 99} {
   200  			ret[fmt.Sprintf("%d", v)+"_percentile"], _ = stats.Percentile(reqtimes, float64(v))
   201  		}
   202  	}
   203  	return ret, nil
   204  }
   205  
   206  // Do the plugin
   207  func Do() {
   208  	var (
   209  		optPrefix    = flag.String("metric-key-prefix", "", "Metric key prefix")
   210  		optFormat    = flag.String("format", "", "Access Log format ('ltsv' or 'apache')")
   211  		optPosFile   = flag.String("posfile", "", "(not necessary to specify it in the usual use case) posfile")
   212  		optNoPosFile = flag.Bool("no-posfile", false, "no position file")
   213  	)
   214  	flag.Usage = func() {
   215  		fmt.Fprintf(os.Stderr, "Usage: %s [OPTION] /path/to/access.log\n", os.Args[0])
   216  		flag.PrintDefaults()
   217  	}
   218  	flag.Parse()
   219  	if flag.NArg() < 1 {
   220  		flag.Usage()
   221  		os.Exit(1)
   222  	}
   223  
   224  	var parser axslogparser.Parser
   225  	switch *optFormat {
   226  	case "":
   227  		parser = nil // guess format by log (default)
   228  	case "ltsv":
   229  		parser = parsers.LTSV
   230  	case "apache":
   231  		parser = parsers.Apache
   232  	default:
   233  		fmt.Fprintf(os.Stderr, "Error: '%s' is invalid format name\n", *optFormat)
   234  		flag.Usage()
   235  		os.Exit(1)
   236  	}
   237  
   238  	mp.NewMackerelPlugin(&AccesslogPlugin{
   239  		prefix:    *optPrefix,
   240  		file:      flag.Args()[0],
   241  		posFile:   *optPosFile,
   242  		noPosFile: *optNoPosFile,
   243  		parser:    parser,
   244  	}).Run()
   245  }