github.com/JBoudou/Itero@v0.1.7/tools/logstat.go (about)

     1  // Itero - Online iterative vote application
     2  // Copyright (C) 2021 Joseph Boudou
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as
     6  // published by the Free Software Foundation, either version 3 of the
     7  // License, or (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"bufio"
    21  	"fmt"
    22  	"os"
    23  	"regexp"
    24  	"sort"
    25  	"time"
    26  )
    27  
    28  type LogStat struct{}
    29  
    30  func (self LogStat) Cmd() string {
    31  	return "logstat"
    32  }
    33  
    34  func (self LogStat) String() string {
    35  	return "Analyse Itero logs."
    36  }
    37  
    38  func init() {
    39  	AddCommand(LogStat{})
    40  }
    41  
    42  func (self LogStat) Run(args []string) {
    43  	timedRegex := regexp.MustCompile(`^(\S{10} \S{15}) \S `)
    44  	fullRegex := regexp.MustCompile(`^\S{10} \S{15} H ([0-9.]{7,15}):\d+ (/\S*) .*(\d{3}) in (\S+)\s$`)
    45  	segmentRegex := regexp.MustCompile(`^((?:/[^/]*[^/\d][^/]*){2,}?)(?:/\d+)*/[a-zA-Z0-9\-_]{9}$`)
    46  
    47  	var firstTime time.Time
    48  	var lastTime time.Time
    49  	total := countTime{}
    50  	bySt := byStatus{}
    51  	byIp := byKeyAndStatus{}
    52  	byRq := byKeyAndStatus{}
    53  	var unparsedLines uint32
    54  
    55  	rd := bufio.NewReader(os.Stdin)
    56  	for {
    57  		line, err := rd.ReadString('\n')
    58  		if err != nil {
    59  			break
    60  		}
    61  
    62  		capture := timedRegex.FindStringSubmatch(line)
    63  		if capture == nil {
    64  			unparsedLines += 1
    65  			continue
    66  		}
    67  		t, err := time.ParseInLocation("2006/01/02 15:04:05.000000", capture[1], time.Local)
    68  		if err != nil {
    69  			unparsedLines += 1
    70  			continue
    71  		}
    72  		if firstTime.IsZero() {
    73  			firstTime = t
    74  		} else {
    75  			lastTime = t
    76  		}
    77  
    78  		capture = fullRegex.FindStringSubmatch(line)
    79  		if capture == nil {
    80  			continue
    81  		}
    82  		dur, err := time.ParseDuration(capture[4])
    83  		if err != nil {
    84  			continue
    85  		}
    86  		total.Add(dur)
    87  		status := capture[3]
    88  		bySt.Add(status, dur)
    89  		byIp.Add(capture[1], status, dur)
    90  
    91  		request := capture[2]
    92  		capture = segmentRegex.FindStringSubmatch(request)
    93  		if capture != nil {
    94  			request = capture[1]
    95  		}
    96  		byRq.Add(request, status, dur)
    97  	}
    98  
    99  	fmt.Printf("Server run: %v.\n", lastTime.Sub(firstTime))
   100  	total.Display("")
   101  	fmt.Printf("%d unparsed lines.\n", unparsedLines)
   102  
   103  	fmt.Println("Status:")
   104  	bySt.Display("  ")
   105  
   106  	fmt.Println("Visitors:")
   107  	byIp.Display("  ")
   108  	fmt.Println("Requests:")
   109  	byRq.Display("  ")
   110  }
   111  
   112  //
   113  // countTime
   114  //
   115  
   116  type countTime struct {
   117  	count    uint32
   118  	duration time.Duration
   119  }
   120  
   121  func (self *countTime) Add(dur time.Duration) {
   122  	self.count += 1
   123  	self.duration += dur
   124  }
   125  
   126  func (self countTime) Display(prefix string) {
   127  	fmt.Printf("%s%d requests in %v (%f req/sec)\n", prefix, self.count, self.duration,
   128  		float64(self.count)/self.duration.Seconds())
   129  }
   130  
   131  //
   132  // byStatus
   133  //
   134  
   135  type status = string
   136  
   137  type byStatus map[status]*countTime
   138  
   139  func (self byStatus) Add(status status, dur time.Duration) {
   140  	ct, ok := self[status]
   141  	if !ok {
   142  		ct = &countTime{}
   143  		self[status] = ct
   144  	}
   145  	ct.Add(dur)
   146  }
   147  
   148  func (self byStatus) Display(prefix string) {
   149  	sorted := make([]status, len(self))
   150  	i := 0
   151  	for key := range self {
   152  		sorted[i] = key
   153  		i += 1
   154  	}
   155  	sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
   156  
   157  	for _, key := range sorted {
   158  		val := self[key]
   159  		val.Display(prefix + key + ": ")
   160  	}
   161  }
   162  
   163  //
   164  // byKeyAndStatus
   165  //
   166  
   167  type byKeyAndStatus_node struct {
   168  	total  countTime
   169  	detail byStatus
   170  }
   171  type byKeyAndStatus map[string]*byKeyAndStatus_node
   172  
   173  func (self byKeyAndStatus) Add(key string, status status, dur time.Duration) {
   174  	bk, ok := self[key]
   175  	if !ok {
   176  		bk = &byKeyAndStatus_node{detail: byStatus{}}
   177  		self[key] = bk
   178  	}
   179  	bk.total.Add(dur)
   180  	bk.detail.Add(status, dur)
   181  }
   182  
   183  func (self byKeyAndStatus) Display(prefix string) {
   184  	sorted := make([]string, len(self))
   185  	i := 0
   186  	for key := range self {
   187  		sorted[i] = key
   188  		i += 1
   189  	}
   190  	sort.Slice(sorted, func(i, j int) bool {
   191  		return self[sorted[i]].total.duration > self[sorted[j]].total.duration
   192  	})
   193  
   194  	for _, key := range sorted {
   195  		val := self[key]
   196  		val.total.Display(prefix + key + ": ")
   197  		val.detail.Display(prefix + "  ")
   198  	}
   199  }