log_sense 1.3.5 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.org +46 -0
- data/Gemfile.lock +4 -4
- data/README.org +24 -10
- data/Rakefile +17 -3
- data/exe/log_sense +24 -16
- data/ip_locations/dbip-country-lite.sqlite3 +0 -0
- data/lib/log_sense/apache_data_cruncher.rb +30 -30
- data/lib/log_sense/apache_log_line_parser.rb +12 -13
- data/lib/log_sense/apache_log_parser.rb +44 -36
- data/lib/log_sense/emitter.rb +518 -15
- data/lib/log_sense/ip_locator.rb +26 -19
- data/lib/log_sense/options_parser.rb +35 -30
- data/lib/log_sense/rails_data_cruncher.rb +8 -4
- data/lib/log_sense/rails_log_parser.rb +108 -100
- data/lib/log_sense/templates/_command_invocation.html.erb +0 -4
- data/lib/log_sense/templates/_command_invocation.txt.erb +4 -3
- data/lib/log_sense/templates/_navigation.html.erb +21 -0
- data/lib/log_sense/templates/_output_table.html.erb +2 -7
- data/lib/log_sense/templates/_output_table.txt.erb +14 -0
- data/lib/log_sense/templates/_performance.html.erb +1 -1
- data/lib/log_sense/templates/_performance.txt.erb +8 -5
- data/lib/log_sense/templates/_report_data.html.erb +2 -2
- data/lib/log_sense/templates/_summary.html.erb +6 -1
- data/lib/log_sense/templates/_summary.txt.erb +11 -8
- data/lib/log_sense/templates/_warning.txt.erb +1 -0
- data/lib/log_sense/templates/apache.html.erb +14 -335
- data/lib/log_sense/templates/apache.txt.erb +22 -0
- data/lib/log_sense/templates/rails.html.erb +13 -174
- data/lib/log_sense/templates/rails.txt.erb +10 -60
- data/lib/log_sense/version.rb +1 -1
- metadata +6 -2
| @@ -7,46 +7,46 @@ module LogSense | |
| 7 7 | 
             
                #
         | 
| 8 8 | 
             
                # parse command line options
         | 
| 9 9 | 
             
                #
         | 
| 10 | 
            -
                def self.parse | 
| 11 | 
            -
                  limit =  | 
| 10 | 
            +
                def self.parse(options)
         | 
| 11 | 
            +
                  limit = 900
         | 
| 12 12 | 
             
                  args = {} 
         | 
| 13 13 |  | 
| 14 14 | 
             
                  opt_parser = OptionParser.new do |opts|
         | 
| 15 | 
            -
                    opts.banner =  | 
| 15 | 
            +
                    opts.banner = 'Usage: log_sense [options] [logfile ...]'
         | 
| 16 16 |  | 
| 17 | 
            -
                    opts.on( | 
| 17 | 
            +
                    opts.on('-tTITLE', '--title=TITLE', String, 'Title to use in the report') do |n|
         | 
| 18 18 | 
             
                      args[:title] = n
         | 
| 19 19 | 
             
                    end
         | 
| 20 20 |  | 
| 21 | 
            -
                    opts.on( | 
| 21 | 
            +
                    opts.on('-fFORMAT', '--input-format=FORMAT', String, 'Input format (either rails or apache)') do |n|
         | 
| 22 22 | 
             
                      args[:input_format] = n
         | 
| 23 23 | 
             
                    end
         | 
| 24 24 |  | 
| 25 | 
            -
                    opts.on( | 
| 26 | 
            -
                      args[:input_file] = n
         | 
| 27 | 
            -
                    end
         | 
| 28 | 
            -
             | 
| 29 | 
            -
                    opts.on("-tFORMAT", "--output-format=FORMAT", String, "Output format: html, org, txt, sqlite. See below for available formats") do |n|
         | 
| 25 | 
            +
                    opts.on('-tFORMAT', '--output-format=FORMAT', String, 'Output format: html, org, txt, sqlite. See below for available formats') do |n|
         | 
| 30 26 | 
             
                      args[:output_format] = n
         | 
| 31 27 | 
             
                    end
         | 
| 32 28 |  | 
| 33 | 
            -
                    opts.on( | 
| 29 | 
            +
                    opts.on('-oOUTPUT_FILE', '--output-file=OUTPUT_FILE', String, 'Output file') do |n|
         | 
| 34 30 | 
             
                      args[:output_file] = n
         | 
| 35 31 | 
             
                    end
         | 
| 36 32 |  | 
| 37 | 
            -
                    opts.on( | 
| 33 | 
            +
                    opts.on('-bDATE', '--begin=DATE', Date, 'Consider entries after or on DATE') do |n|
         | 
| 38 34 | 
             
                      args[:from_date] = n
         | 
| 39 35 | 
             
                    end
         | 
| 40 36 |  | 
| 41 | 
            -
                    opts.on( | 
| 37 | 
            +
                    opts.on('-eDATE', '--end=DATE', Date, 'Consider entries before or on DATE') do |n|
         | 
| 42 38 | 
             
                      args[:to_date] = n
         | 
| 43 39 | 
             
                    end
         | 
| 44 40 |  | 
| 45 | 
            -
                    opts.on( | 
| 41 | 
            +
                    opts.on('-lN', '--limit=N', Integer, "Limit to the N most requested resources (defaults to #{limit})") do |n|
         | 
| 46 42 | 
             
                      args[:limit] = n
         | 
| 47 43 | 
             
                    end
         | 
| 48 44 |  | 
| 49 | 
            -
                    opts.on( | 
| 45 | 
            +
                    opts.on('-wWIDTH', '--width=WIDTH', Integer, 'Maximum width of URL and description columns in text reports') do |n|
         | 
| 46 | 
            +
                      args[:width] = n
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    opts.on('-cPOLICY', '--crawlers=POLICY', String, 'Decide what to do with crawlers (applies to Apache Logs)') do |n|
         | 
| 50 50 | 
             
                      case n
         | 
| 51 51 | 
             
                      when 'only'
         | 
| 52 52 | 
             
                        args[:only_crawlers] = true
         | 
| @@ -55,30 +55,34 @@ module LogSense | |
| 55 55 | 
             
                      end
         | 
| 56 56 | 
             
                    end
         | 
| 57 57 |  | 
| 58 | 
            -
                    opts.on( | 
| 58 | 
            +
                    opts.on('-ns', '--no-selfpoll', 'Ignore self poll entries (requests from ::1; applies to Apache Logs)') do
         | 
| 59 59 | 
             
                      args[:no_selfpoll] = true
         | 
| 60 60 | 
             
                    end
         | 
| 61 61 |  | 
| 62 | 
            -
                    opts.on( | 
| 62 | 
            +
                    opts.on('--verbose', 'Inform about progress (prints to STDERR)') do
         | 
| 63 | 
            +
                      args[:verbose] = true
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    opts.on('-v', '--version', 'Prints version information') do
         | 
| 63 67 | 
             
                      puts "log_sense version #{LogSense::VERSION}"
         | 
| 64 | 
            -
                      puts  | 
| 65 | 
            -
                      puts  | 
| 68 | 
            +
                      puts 'Copyright (C) 2021 Shair.Tech'
         | 
| 69 | 
            +
                      puts 'Distributed under the terms of the MIT license'
         | 
| 66 70 | 
             
                      exit
         | 
| 67 71 | 
             
                    end
         | 
| 68 72 |  | 
| 69 | 
            -
                    opts.on( | 
| 73 | 
            +
                    opts.on('-h', '--help', 'Prints this help') do
         | 
| 70 74 | 
             
                      puts opts
         | 
| 71 | 
            -
                      puts  | 
| 75 | 
            +
                      puts ''
         | 
| 72 76 | 
             
                      puts "This is version #{LogSense::VERSION}"
         | 
| 73 77 |  | 
| 74 | 
            -
                      puts  | 
| 75 | 
            -
                      puts  | 
| 76 | 
            -
                      pathname = File.join(File.dirname(__FILE__),  | 
| 77 | 
            -
                      templates = Dir.glob(pathname).select { |x| ! | 
| 78 | 
            -
                      components = templates.map { |x| File.basename(x).split  | 
| 78 | 
            +
                      puts ''
         | 
| 79 | 
            +
                      puts 'Output formats'
         | 
| 80 | 
            +
                      pathname = File.join(File.dirname(__FILE__), 'templates', '*')
         | 
| 81 | 
            +
                      templates = Dir.glob(pathname).select { |x| !File.basename(x).start_with?(/_|#/) && !File.basename(x).end_with?('~') }
         | 
| 82 | 
            +
                      components = templates.map { |x| File.basename(x).split '.' }.group_by { |x| x[0] }
         | 
| 79 83 | 
             
                      components.each do |k, vs|
         | 
| 80 84 | 
             
                        puts "#{k} parsing can produce the following outputs:"
         | 
| 81 | 
            -
                        puts  | 
| 85 | 
            +
                        puts '  - sqlite'
         | 
| 82 86 | 
             
                        vs.each do |v|
         | 
| 83 87 | 
             
                          puts "  - #{v[1]}"
         | 
| 84 88 | 
             
                        end
         | 
| @@ -91,13 +95,14 @@ module LogSense | |
| 91 95 | 
             
                  opt_parser.parse!(options)
         | 
| 92 96 |  | 
| 93 97 | 
             
                  args[:limit] ||= limit
         | 
| 94 | 
            -
                  args[:input_format] ||=  | 
| 95 | 
            -
                  args[:output_format] ||=  | 
| 98 | 
            +
                  args[:input_format] ||= 'apache'
         | 
| 99 | 
            +
                  args[:output_format] ||= 'html'
         | 
| 96 100 | 
             
                  args[:ignore_crawlers] ||= false
         | 
| 97 101 | 
             
                  args[:only_crawlers] ||= false
         | 
| 98 102 | 
             
                  args[:no_selfpoll] ||= false
         | 
| 103 | 
            +
                  args[:verbose] ||= false
         | 
| 99 104 |  | 
| 100 | 
            -
                   | 
| 105 | 
            +
                  args
         | 
| 101 106 | 
             
                end
         | 
| 102 107 | 
             
              end
         | 
| 103 108 | 
             
            end
         | 
| @@ -7,7 +7,7 @@ module LogSense | |
| 7 7 | 
             
                # @ variables are automatically put in the returned data
         | 
| 8 8 | 
             
                #
         | 
| 9 9 |  | 
| 10 | 
            -
                def self.crunch db, options = { limit:  | 
| 10 | 
            +
                def self.crunch db, options = { limit: 900 }
         | 
| 11 11 | 
             
                  first_day_s = db.execute "SELECT started_at from Event where started_at not NULL order by started_at limit 1"
         | 
| 12 12 | 
             
                  # we could use ended_at to cover the full activity period, but I prefer started_at
         | 
| 13 13 | 
             
                  # with the meaning that the monitor event initiation
         | 
| @@ -19,9 +19,10 @@ module LogSense | |
| 19 19 | 
             
                  @last_day =  last_day_s&.first&.first ? Date.parse(last_day_s[0][0]) : nil
         | 
| 20 20 |  | 
| 21 21 | 
             
                  @total_days = 0
         | 
| 22 | 
            -
                  if @first_day  | 
| 23 | 
            -
             | 
| 24 | 
            -
                   | 
| 22 | 
            +
                  @total_days = (@last_day - @first_day).to_i if @first_day && @last_day
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  # TODO should also look into Error
         | 
| 25 | 
            +
                  @source_files = db.execute "SELECT distinct(source_file) from Event"
         | 
| 25 26 |  | 
| 26 27 | 
             
                  @log_size = db.execute "SELECT count(started_at) from Event"
         | 
| 27 28 | 
             
                  @log_size = @log_size[0][0]
         | 
| @@ -103,6 +104,9 @@ module LogSense | |
| 103 104 |  | 
| 104 105 | 
             
                  @ips = db.execute "SELECT ip, count(ip) from Event where #{filter} group by ip order by count(ip) desc limit #{options[:limit]}"
         | 
| 105 106 |  | 
| 107 | 
            +
                  @streaks = db.execute 'SELECT ip, substr(started_at, 1, 10), url from Event order by ip, started_at'
         | 
| 108 | 
            +
                  data = {}
         | 
| 109 | 
            +
             | 
| 106 110 | 
             
                  @performance = db.execute "SELECT distinct(controller), count(controller), printf(\"%.2f\", min(duration_total_ms)), printf(\"%.2f\", avg(duration_total_ms)), printf(\"%.2f\", max(duration_total_ms)) from Event group by controller order by controller"
         | 
| 107 111 |  | 
| 108 112 | 
             
                  @fatal = db.execute ("SELECT strftime(\"%Y-%m-%d %H:%M\", started_at), ip, url, error.description, event.log_id FROM Event JOIN Error ON event.log_id == error.log_id WHERE exit_status == 'F'") || [[]]
         | 
| @@ -2,10 +2,8 @@ require 'sqlite3' | |
| 2 2 |  | 
| 3 3 | 
             
            module LogSense
         | 
| 4 4 | 
             
              module RailsLogParser
         | 
| 5 | 
            -
                def self.parse | 
| 6 | 
            -
                   | 
| 7 | 
            -
             | 
| 8 | 
            -
                  db = SQLite3::Database.new ":memory:"
         | 
| 5 | 
            +
                def self.parse(streams, options = {})
         | 
| 6 | 
            +
                  db = SQLite3::Database.new ':memory:'
         | 
| 9 7 | 
             
                  db.execute 'CREATE TABLE IF NOT EXISTS Event(
         | 
| 10 8 | 
             
                     id INTEGER PRIMARY KEY AUTOINCREMENT,
         | 
| 11 9 | 
             
                     exit_status TEXT,
         | 
| @@ -17,12 +15,14 @@ module LogSense | |
| 17 15 | 
             
                     url TEXT,
         | 
| 18 16 | 
             
                     controller TEXT,
         | 
| 19 17 | 
             
                     html_verb TEXT,
         | 
| 20 | 
            -
                     status INTEGER, | 
| 18 | 
            +
                     status INTEGER,
         | 
| 21 19 | 
             
                     duration_total_ms FLOAT,
         | 
| 22 20 | 
             
                     duration_views_ms FLOAT,
         | 
| 23 21 | 
             
                     duration_ar_ms FLOAT,
         | 
| 24 22 | 
             
                     allocations INTEGER,
         | 
| 25 | 
            -
                     comment TEXT
         | 
| 23 | 
            +
                     comment TEXT,
         | 
| 24 | 
            +
                     source_file TEXT,
         | 
| 25 | 
            +
                     line_number INTEGER
         | 
| 26 26 | 
             
                  )'
         | 
| 27 27 |  | 
| 28 28 | 
             
                  ins = db.prepare("insert into Event(
         | 
| @@ -35,30 +35,34 @@ module LogSense | |
| 35 35 | 
             
                     url,
         | 
| 36 36 | 
             
                     controller,
         | 
| 37 37 | 
             
                     html_verb,
         | 
| 38 | 
            -
                     status, | 
| 38 | 
            +
                     status,
         | 
| 39 39 | 
             
                     duration_total_ms,
         | 
| 40 40 | 
             
                     duration_views_ms,
         | 
| 41 41 | 
             
                     duration_ar_ms,
         | 
| 42 42 | 
             
                     allocations,
         | 
| 43 | 
            -
                     comment
         | 
| 43 | 
            +
                     comment,
         | 
| 44 | 
            +
                     source_file,
         | 
| 45 | 
            +
                     line_number
         | 
| 44 46 | 
             
                  )
         | 
| 45 | 
            -
                  values (#{Array.new( | 
| 47 | 
            +
                  values (#{Array.new(17, '?').join(', ')})")
         | 
| 46 48 |  | 
| 47 | 
            -
                  
         | 
| 48 49 | 
             
                  db.execute 'CREATE TABLE IF NOT EXISTS Error(
         | 
| 49 50 | 
             
                     id INTEGER PRIMARY KEY AUTOINCREMENT,
         | 
| 50 51 | 
             
                     log_id TEXT,
         | 
| 51 52 | 
             
                     context TEXT,
         | 
| 52 | 
            -
                     description TEXT
         | 
| 53 | 
            +
                     description TEXT,
         | 
| 54 | 
            +
                     filename TEXT,
         | 
| 55 | 
            +
                     line_number INTEGER
         | 
| 53 56 | 
             
                  )'
         | 
| 54 57 |  | 
| 55 58 | 
             
                  ins_error = db.prepare("insert into Error(
         | 
| 56 59 | 
             
                     log_id,
         | 
| 57 60 | 
             
                     context,
         | 
| 58 | 
            -
                     description
         | 
| 61 | 
            +
                     description,
         | 
| 62 | 
            +
                     filename,
         | 
| 63 | 
            +
                     line_number
         | 
| 59 64 | 
             
                  )
         | 
| 60 | 
            -
                  values (?, ?, ?)")
         | 
| 61 | 
            -
                  
         | 
| 65 | 
            +
                  values (?, ?, ?, ?, ?)")
         | 
| 62 66 |  | 
| 63 67 | 
             
                  # requests in the log might be interleaved.
         | 
| 64 68 | 
             
                  #
         | 
| @@ -79,94 +83,101 @@ module LogSense | |
| 79 83 | 
             
                  # and they appears in the order shown above: started, processing, ...
         | 
| 80 84 | 
             
                  #
         | 
| 81 85 | 
             
                  # Different requests might be interleaved, of course
         | 
| 82 | 
            -
                  
         | 
| 83 | 
            -
                   | 
| 84 | 
            -
                     | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
                    data = self.match_and_process_error line
         | 
| 88 | 
            -
                    if data
         | 
| 89 | 
            -
                      ins_error.execute(data[:log_id], data[:context], data[:description])
         | 
| 90 | 
            -
                      next
         | 
| 91 | 
            -
                    end
         | 
| 92 | 
            -
                    
         | 
| 93 | 
            -
                    data = self.match_and_process_start line
         | 
| 94 | 
            -
                    if data
         | 
| 95 | 
            -
                      id = data[:log_id]
         | 
| 96 | 
            -
                      pending[id] = data.merge (pending[id] || {})
         | 
| 97 | 
            -
                      next
         | 
| 98 | 
            -
                    end
         | 
| 99 | 
            -
             | 
| 100 | 
            -
                    data = self.match_and_process_processing_by line
         | 
| 101 | 
            -
                    if data
         | 
| 102 | 
            -
                      id = data[:log_id]
         | 
| 103 | 
            -
                      pending[id] = data.merge (pending[id] || {})
         | 
| 104 | 
            -
                      next
         | 
| 105 | 
            -
                    end
         | 
| 106 | 
            -
             | 
| 107 | 
            -
                    data = self.match_and_process_fatal line
         | 
| 108 | 
            -
                    if data
         | 
| 109 | 
            -
                      id = data[:log_id]
         | 
| 110 | 
            -
                      # it might as well be that the first event started before
         | 
| 111 | 
            -
                      # the log.  With this, we make sure we add only events whose
         | 
| 112 | 
            -
                      # start was logged and parsed
         | 
| 113 | 
            -
                      if pending[id]
         | 
| 114 | 
            -
                        event = data.merge (pending[id] || {})
         | 
| 86 | 
            +
                  #
         | 
| 87 | 
            +
                  streams.each do |stream|
         | 
| 88 | 
            +
                    stream.readlines.each_with_index do |line, line_number|
         | 
| 89 | 
            +
                      filename = stream == $stdin ? "stdin" : stream.path
         | 
| 115 90 |  | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
                          event[:started_at],
         | 
| 119 | 
            -
                          event[:ended_at],
         | 
| 120 | 
            -
                          event[:log_id],
         | 
| 121 | 
            -
                          event[:ip],
         | 
| 122 | 
            -
                          unique_visitor_id(event),
         | 
| 123 | 
            -
                          event[:url],
         | 
| 124 | 
            -
                          event[:controller],
         | 
| 125 | 
            -
                          event[:html_verb],
         | 
| 126 | 
            -
                          event[:status],
         | 
| 127 | 
            -
                          event[:duration_total_ms],
         | 
| 128 | 
            -
                          event[:duration_views_ms],
         | 
| 129 | 
            -
                          event[:duration_ar_ms],
         | 
| 130 | 
            -
                          event[:allocations],
         | 
| 131 | 
            -
                          event[:comment]
         | 
| 132 | 
            -
                        )
         | 
| 91 | 
            +
                      # I and F for completed requests, [ is for error messages
         | 
| 92 | 
            +
                      next if line[0] != 'I' and line[0] != 'F' and line[0] != '['
         | 
| 133 93 |  | 
| 134 | 
            -
             | 
| 94 | 
            +
                      data = match_and_process_error line
         | 
| 95 | 
            +
                      if data
         | 
| 96 | 
            +
                        ins_error.execute(data[:log_id], data[:context], data[:description], filename, line_number)
         | 
| 97 | 
            +
                        next
         | 
| 98 | 
            +
                      end
         | 
| 99 | 
            +
                      
         | 
| 100 | 
            +
                      data = match_and_process_start line
         | 
| 101 | 
            +
                      if data
         | 
| 102 | 
            +
                        id = data[:log_id]
         | 
| 103 | 
            +
                        pending[id] = data.merge(pending[id] || {})
         | 
| 104 | 
            +
                        next
         | 
| 135 105 | 
             
                      end
         | 
| 136 | 
            -
                    end
         | 
| 137 | 
            -
             | 
| 138 | 
            -
                    data = self.match_and_process_completed line
         | 
| 139 | 
            -
                    if data
         | 
| 140 | 
            -
                      id = data[:log_id]
         | 
| 141 106 |  | 
| 142 | 
            -
                       | 
| 143 | 
            -
                       | 
| 144 | 
            -
             | 
| 145 | 
            -
             | 
| 146 | 
            -
                         | 
| 107 | 
            +
                      data = match_and_process_processing_by line
         | 
| 108 | 
            +
                      if data
         | 
| 109 | 
            +
                        id = data[:log_id]
         | 
| 110 | 
            +
                        pending[id] = data.merge(pending[id] || {})
         | 
| 111 | 
            +
                        next
         | 
| 112 | 
            +
                      end
         | 
| 147 113 |  | 
| 148 | 
            -
             | 
| 149 | 
            -
             | 
| 150 | 
            -
             | 
| 151 | 
            -
             | 
| 152 | 
            -
             | 
| 153 | 
            -
             | 
| 154 | 
            -
             | 
| 155 | 
            -
                          event[ | 
| 156 | 
            -
             | 
| 157 | 
            -
                           | 
| 158 | 
            -
             | 
| 159 | 
            -
             | 
| 160 | 
            -
             | 
| 161 | 
            -
             | 
| 162 | 
            -
             | 
| 163 | 
            -
             | 
| 164 | 
            -
             | 
| 114 | 
            +
                      data = match_and_process_fatal line
         | 
| 115 | 
            +
                      if data
         | 
| 116 | 
            +
                        id = data[:log_id]
         | 
| 117 | 
            +
                        # it might as well be that the first event started before
         | 
| 118 | 
            +
                        # the log.  With this, we make sure we add only events whose
         | 
| 119 | 
            +
                        # start was logged and parsed
         | 
| 120 | 
            +
                        if pending[id]
         | 
| 121 | 
            +
                          event = data.merge(pending[id] || {})
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                          ins.execute(
         | 
| 124 | 
            +
                            event[:exit_status],
         | 
| 125 | 
            +
                            event[:started_at],
         | 
| 126 | 
            +
                            event[:ended_at],
         | 
| 127 | 
            +
                            event[:log_id],
         | 
| 128 | 
            +
                            event[:ip],
         | 
| 129 | 
            +
                            unique_visitor_id(event),
         | 
| 130 | 
            +
                            event[:url],
         | 
| 131 | 
            +
                            event[:controller],
         | 
| 132 | 
            +
                            event[:html_verb],
         | 
| 133 | 
            +
                            event[:status],
         | 
| 134 | 
            +
                            event[:duration_total_ms],
         | 
| 135 | 
            +
                            event[:duration_views_ms],
         | 
| 136 | 
            +
                            event[:duration_ar_ms],
         | 
| 137 | 
            +
                            event[:allocations],
         | 
| 138 | 
            +
                            event[:comment],
         | 
| 139 | 
            +
                            filename,
         | 
| 140 | 
            +
                            line_number
         | 
| 141 | 
            +
                          )
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                          pending.delete(id)
         | 
| 144 | 
            +
                        end
         | 
| 145 | 
            +
                      end
         | 
| 165 146 |  | 
| 166 | 
            -
             | 
| 147 | 
            +
                      data = self.match_and_process_completed line
         | 
| 148 | 
            +
                      if data
         | 
| 149 | 
            +
                        id = data[:log_id]
         | 
| 150 | 
            +
             | 
| 151 | 
            +
                        # it might as well be that the first event started before
         | 
| 152 | 
            +
                        # the log.  With this, we make sure we add only events whose
         | 
| 153 | 
            +
                        # start was logged and parsed
         | 
| 154 | 
            +
                        if pending[id]
         | 
| 155 | 
            +
                          event = data.merge (pending[id] || {})
         | 
| 156 | 
            +
             | 
| 157 | 
            +
                          ins.execute(
         | 
| 158 | 
            +
                            event[:exit_status],
         | 
| 159 | 
            +
                            event[:started_at],
         | 
| 160 | 
            +
                            event[:ended_at],
         | 
| 161 | 
            +
                            event[:log_id],
         | 
| 162 | 
            +
                            event[:ip],
         | 
| 163 | 
            +
                            unique_visitor_id(event),
         | 
| 164 | 
            +
                            event[:url],
         | 
| 165 | 
            +
                            event[:controller],
         | 
| 166 | 
            +
                            event[:html_verb],
         | 
| 167 | 
            +
                            event[:status],
         | 
| 168 | 
            +
                            event[:duration_total_ms],
         | 
| 169 | 
            +
                            event[:duration_views_ms],
         | 
| 170 | 
            +
                            event[:duration_ar_ms],
         | 
| 171 | 
            +
                            event[:allocations],
         | 
| 172 | 
            +
                            event[:comment],
         | 
| 173 | 
            +
                            filename,
         | 
| 174 | 
            +
                            line_number
         | 
| 175 | 
            +
                          )
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                          pending.delete(id)
         | 
| 178 | 
            +
                        end
         | 
| 167 179 | 
             
                      end
         | 
| 168 180 | 
             
                    end
         | 
| 169 | 
            -
             | 
| 170 181 | 
             
                  end
         | 
| 171 182 |  | 
| 172 183 | 
             
                  db
         | 
| @@ -226,7 +237,7 @@ module LogSense | |
| 226 237 | 
             
                # I, [2021-12-06T14:28:19.736545 #2804090]  INFO -- : [34091cb5-3e7b-4042-aaf8-6c6510d3f14c] Completed 500 Internal Server Error in 66ms (ActiveRecord: 8.0ms | Allocations: 24885)
         | 
| 227 238 | 
             
                COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\]  INFO -- : \[#{ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \((Views: (?<views>#{MSECS})ms \| )?ActiveRecord: (?<arec>#{MSECS})ms( \| Allocations: (?<alloc>[0-9]+))?\)/
         | 
| 228 239 |  | 
| 229 | 
            -
                def self.match_and_process_completed | 
| 240 | 
            +
                def self.match_and_process_completed(line)
         | 
| 230 241 | 
             
                  matchdata = (COMPLETED_REGEXP.match line)
         | 
| 231 242 | 
             
                  # exit_status = matchdata[:status].to_i == 500 ? "E" : "I"
         | 
| 232 243 | 
             
                  if matchdata
         | 
| @@ -267,7 +278,7 @@ module LogSense | |
| 267 278 | 
             
                # F, [2021-12-04T00:34:05.839269 #2735058] FATAL -- : [3a16162e-a6a5-435e-a9d8-c4df5dc0f728] actionpack (5.2.4.4) lib/action_dispatch/middleware/debug_exceptions.rb:65:in `call'
         | 
| 268 279 | 
             
                FATAL_REGEXP = /F, \[#{TIMESTAMP} #[0-9]+\] FATAL -- : \[#{ID}\] (?<comment>.*)$/
         | 
| 269 280 |  | 
| 270 | 
            -
                def self.match_and_process_fatal | 
| 281 | 
            +
                def self.match_and_process_fatal(line)
         | 
| 271 282 | 
             
                  matchdata = FATAL_REGEXP.match line
         | 
| 272 283 | 
             
                  if matchdata
         | 
| 273 284 | 
             
                    {
         | 
| @@ -281,11 +292,8 @@ module LogSense | |
| 281 292 | 
             
                end
         | 
| 282 293 |  | 
| 283 294 | 
             
                # generate a unique visitor id from an event
         | 
| 284 | 
            -
                def self.unique_visitor_id | 
| 295 | 
            +
                def self.unique_visitor_id(event)
         | 
| 285 296 | 
             
                  "#{DateTime.parse(event[:started_at] || event[:ended_at] || "1970-01-01").strftime("%Y-%m-%d")} #{event[:ip]}"
         | 
| 286 297 | 
             
                end
         | 
| 287 | 
            -
             | 
| 288 298 | 
             
              end
         | 
| 289 | 
            -
             | 
| 290 299 | 
             
            end
         | 
| 291 | 
            -
             | 
| @@ -4,10 +4,6 @@ | |
| 4 4 | 
             
                  <th>CLI Command</th>
         | 
| 5 5 | 
             
                  <td><code><%= data[:command] %></code></td>
         | 
| 6 6 | 
             
                </tr>
         | 
| 7 | 
            -
                <tr>
         | 
| 8 | 
            -
                  <th>Input file</th>
         | 
| 9 | 
            -
                  <td><code><%= (data[:log_file] || "stdin") %></code></td>
         | 
| 10 | 
            -
                </tr>
         | 
| 11 7 | 
             
                <tr>
         | 
| 12 8 | 
             
                  <th>Ignore crawlers</th>
         | 
| 13 9 | 
             
                  <td><code><%= options[:ignore_crawlers] %></code></td></tr>
         | 
| @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            <nav>
         | 
| 2 | 
            +
              <h2>Navigation</h2>
         | 
| 3 | 
            +
              <ul class="no-bullet">
         | 
| 4 | 
            +
                <% (["Summary", "Log Structure"] +
         | 
| 5 | 
            +
                    menus +
         | 
| 6 | 
            +
                    ["Command Invocation", "Performance"]).each do |item| %>
         | 
| 7 | 
            +
                  <li class="nav-item">
         | 
| 8 | 
            +
                    <a href="#<%= Emitter::slugify item %>" data-close>
         | 
| 9 | 
            +
                      <%= item %>
         | 
| 10 | 
            +
                    </a>
         | 
| 11 | 
            +
                  </li>
         | 
| 12 | 
            +
                <% end %>
         | 
| 13 | 
            +
              </ul>
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              <p>
         | 
| 16 | 
            +
                Generated by
         | 
| 17 | 
            +
                <a href="https://github.com/avillafiorita/log_sense">LogSense</a> <br />
         | 
| 18 | 
            +
                on <%= DateTime.now.strftime("%Y-%m-%d %H:%M") %>.<br />
         | 
| 19 | 
            +
                <a href='https://db-ip.com'>IP Geolocation by DB-IP</a>
         | 
| 20 | 
            +
              </p>
         | 
| 21 | 
            +
            </nav>
         | 
| @@ -1,9 +1,3 @@ | |
| 1 | 
            -
            <%
         | 
| 2 | 
            -
            def slugify string
         | 
| 3 | 
            -
              string.downcase.gsub(/ +/, '-')
         | 
| 4 | 
            -
            end
         | 
| 5 | 
            -
            %>
         | 
| 6 | 
            -
             | 
| 7 1 | 
             
            <table id="table-<%= index %>" class="table unstriped">
         | 
| 8 2 | 
             
              <thead>
         | 
| 9 3 | 
             
                <tr>
         | 
| @@ -19,9 +13,10 @@ end | |
| 19 13 | 
             
             $(document).ready(function(){
         | 
| 20 14 | 
             
               $('#table-<%= index %>').dataTable({
         | 
| 21 15 | 
             
                 data: data_<%= index %>,
         | 
| 16 | 
            +
                 <%= report[:datatable_options] + "," if report[:datatable_options] %>
         | 
| 22 17 | 
             
                 columns: [
         | 
| 23 18 | 
             
                   <% report[:header].each do |header| %>
         | 
| 24 | 
            -
                   { data: '<%= header %>', className: '<%= slugify(header) %>' },
         | 
| 19 | 
            +
                   { data: '<%= header %>', className: '<%= Emitter::slugify(header) %>' },
         | 
| 25 20 | 
             
                   <% end %>
         | 
| 26 21 | 
             
                 ]
         | 
| 27 22 | 
             
               });
         | 
| @@ -0,0 +1,14 @@ | |
| 1 | 
            +
            <%=
         | 
| 2 | 
            +
            # shortens long URLs and long descriptions
         | 
| 3 | 
            +
            shortened = Emitter::shorten(report[:rows], report[:header], data[:width])
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            # build and style the table
         | 
| 6 | 
            +
            table = Terminal::Table.new headings: report[:header], rows: shortened
         | 
| 7 | 
            +
            table.style = { border_i: "|" }
         | 
| 8 | 
            +
            columns = report[:header].size - 1
         | 
| 9 | 
            +
            (0..columns).map do |i|
         | 
| 10 | 
            +
              table.align_column(i, report[:column_alignment][i] || :left)
         | 
| 11 | 
            +
            end
         | 
| 12 | 
            +
            # return it
         | 
| 13 | 
            +
            table
         | 
| 14 | 
            +
            %>
         | 
| @@ -15,7 +15,7 @@ | |
| 15 15 | 
             
                <%= data[:log_size] %> <span class="stats-list-label">Events</span>
         | 
| 16 16 | 
             
              </li>
         | 
| 17 17 | 
             
              <li class="stats-list-positive">
         | 
| 18 | 
            -
                 | 
| 18 | 
            +
                <%= "%.2f" % (data[:log_size] / data[:duration]) %>
         | 
| 19 19 | 
             
                <span class="stats-list-label">Parsed Events/sec</span>
         | 
| 20 20 | 
             
              </li>
         | 
| 21 21 | 
             
            </ul>
         | 
| @@ -1,9 +1,12 @@ | |
| 1 1 | 
             
            <%=
         | 
| 2 | 
            -
            table = Terminal::Table.new rows: [ | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 2 | 
            +
            table = Terminal::Table.new rows: [
         | 
| 3 | 
            +
              ["Analysis started at", data[:started_at].to_s ],
         | 
| 4 | 
            +
              ["Analysis ended at", data[:ended_at].to_s ],
         | 
| 5 | 
            +
              ["Duration", "%02d:%02d" % [data[:duration] / 60, data[:duration] % 60] ],
         | 
| 6 | 
            +
              ["Events", "%9d" % data[:log_size] ],
         | 
| 7 | 
            +
              ["Parsed events/sec", "%.2f" % (data[:log_size] / data[:duration]) ]
         | 
| 8 | 
            +
            ]
         | 
| 9 | 
            +
            table.style = { border_i: "|" }
         | 
| 7 10 | 
             
            table.align_column(2, :right)
         | 
| 8 11 | 
             
            table
         | 
| 9 12 | 
             
            %>
         | 
| @@ -1,10 +1,10 @@ | |
| 1 1 | 
             
            <script>
         | 
| 2 | 
            -
             /* this is used both by Vega and DataTable  | 
| 2 | 
            +
             /* this is used both by Vega and DataTable for <%= report[:title] %>*/
         | 
| 3 3 | 
             
             data_<%= index %> = [
         | 
| 4 4 | 
             
               <% report[:rows].each do |row| %>
         | 
| 5 5 | 
             
               {
         | 
| 6 6 | 
             
                 <% report[:header].each_with_index do |h, i| %>
         | 
| 7 | 
            -
                 "<%= h %>": <%=  | 
| 7 | 
            +
                 "<%= h %>": "<%= Emitter::process row[i] %>",
         | 
| 8 8 | 
             
                 <% end %>
         | 
| 9 9 | 
             
               },
         | 
| 10 10 | 
             
               <% end %>
         | 
| @@ -17,7 +17,12 @@ | |
| 17 17 | 
             
                <%= data[:total_unique_visits] %> <span class="stats-list-label">Unique Visits</span>
         | 
| 18 18 | 
             
              </li>
         | 
| 19 19 | 
             
              <li class="stats-list-negative">
         | 
| 20 | 
            -
                 | 
| 20 | 
            +
                <% days = data[:last_day_in_analysis] - data[:first_day_in_analysis] %>
         | 
| 21 | 
            +
                <%= days > 0 ? "%d" % (data[:total_unique_visits] / days) : "N/A" %>
         | 
| 21 22 | 
             
                <span class="stats-list-label">Unique Visits / Day</span>
         | 
| 22 23 | 
             
              </li>
         | 
| 24 | 
            +
              <li class="stats-list-negative">
         | 
| 25 | 
            +
                <%= data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A" %>
         | 
| 26 | 
            +
                <span class="stats-list-label">Hits / Unique Visitor</span>
         | 
| 27 | 
            +
              </li>
         | 
| 23 28 | 
             
            </ul>
         | 
| @@ -1,10 +1,13 @@ | |
| 1 1 | 
             
            <%=
         | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
             | 
| 9 | 
            -
              | 
| 2 | 
            +
            table = Terminal::Table.new rows: [
         | 
| 3 | 
            +
              ["From", data[:first_day_in_analysis]],
         | 
| 4 | 
            +
              ["To", data[:last_day_in_analysis]],
         | 
| 5 | 
            +
              ["Days", data[:total_days_in_analysis]],
         | 
| 6 | 
            +
              ["Hits", data[:total_hits]],
         | 
| 7 | 
            +
              ["Unique Visits", data[:total_unique_visits]],
         | 
| 8 | 
            +
              ["Unique Visits / Day", data[:total_days_in_analysis] > 0 ? "%d" % (data[:total_unique_visits] / data[:total_days_in_analysis]) : "N/A"],
         | 
| 9 | 
            +
              ["Hits/Unique Visitor", data[:total_unique_visits] != 0 ? data[:total_hits] / data[:total_unique_visits] : "N/A"]
         | 
| 10 | 
            +
            ]
         | 
| 11 | 
            +
            table.style = { border_i: "|" }
         | 
| 12 | 
            +
            table
         | 
| 10 13 | 
             
            %>
         | 
| @@ -0,0 +1 @@ | |
| 1 | 
            +
            >>>> URLs IN THIS FILE ARE NOT SANITIZED AND MIGHT BE UNSAFE TO OPEN <<<<
         |