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 }