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
    
        data/lib/log_sense/emitter.rb
    CHANGED
    
    | @@ -1,30 +1,29 @@ | |
| 1 | 
            +
            # coding: utf-8
         | 
| 1 2 | 
             
            require 'terminal-table'
         | 
| 2 3 | 
             
            require 'json'
         | 
| 3 4 | 
             
            require 'erb'
         | 
| 4 5 | 
             
            require 'ostruct'
         | 
| 5 | 
            -
             | 
| 6 6 | 
             
            module LogSense
         | 
| 7 | 
            +
              #
         | 
| 8 | 
            +
              # Emit Data
         | 
| 9 | 
            +
              #
         | 
| 7 10 | 
             
              module Emitter
         | 
| 8 | 
            -
             | 
| 9 | 
            -
                #
         | 
| 10 | 
            -
                # Emit Data
         | 
| 11 | 
            -
                #
         | 
| 12 11 | 
             
                def self.emit data = {}, options = {}
         | 
| 13 | 
            -
                  @input_format = options[:input_format] ||  | 
| 14 | 
            -
                  @output_format = options[:output_format] ||  | 
| 12 | 
            +
                  @input_format = options[:input_format] || 'apache'
         | 
| 13 | 
            +
                  @output_format = options[:output_format] || 'html'
         | 
| 15 14 |  | 
| 16 15 | 
             
                  # for the ERB binding
         | 
| 16 | 
            +
                  @reports = method("#{@input_format}_report_specification".to_sym).call(data)
         | 
| 17 17 | 
             
                  @data = data
         | 
| 18 18 | 
             
                  @options = options
         | 
| 19 19 |  | 
| 20 20 | 
             
                  # determine the main template to read
         | 
| 21 | 
            -
                  @template = File.join(File.dirname(__FILE__),  | 
| 21 | 
            +
                  @template = File.join(File.dirname(__FILE__), 'templates', "#{@input_format}.#{@output_format}.erb")
         | 
| 22 22 | 
             
                  erb_template = File.read @template
         | 
| 23 | 
            -
                  
         | 
| 24 23 | 
             
                  output = ERB.new(erb_template).result(binding)
         | 
| 25 24 |  | 
| 26 25 | 
             
                  if options[:output_file]
         | 
| 27 | 
            -
                    file = File.open options[:output_file],  | 
| 26 | 
            +
                    file = File.open options[:output_file], 'w'
         | 
| 28 27 | 
             
                    file.write output
         | 
| 29 28 | 
             
                    file.close
         | 
| 30 29 | 
             
                  else
         | 
| @@ -32,20 +31,524 @@ module LogSense | |
| 32 31 | 
             
                  end
         | 
| 33 32 | 
             
                end
         | 
| 34 33 |  | 
| 35 | 
            -
                 | 
| 36 | 
            -
             | 
| 37 | 
            -
                def self.render(template, vars)
         | 
| 38 | 
            -
                  @template = File.join(File.dirname(__FILE__), "templates", "_#{template}")
         | 
| 34 | 
            +
                def self.render(template, vars = {})
         | 
| 35 | 
            +
                  @template = File.join(File.dirname(__FILE__), 'templates', "_#{template}")
         | 
| 39 36 | 
             
                  erb_template = File.read @template
         | 
| 40 37 | 
             
                  ERB.new(erb_template).result(OpenStruct.new(vars).instance_eval { binding })
         | 
| 41 38 | 
             
                end
         | 
| 42 39 |  | 
| 43 40 | 
             
                def self.escape_javascript(string)
         | 
| 44 | 
            -
                  js_escape_map = { | 
| 41 | 
            +
                  js_escape_map = {
         | 
| 42 | 
            +
                    '<' => '<',
         | 
| 43 | 
            +
                    '</' => '</',
         | 
| 44 | 
            +
                    '\r\n' => '\\r\\n',
         | 
| 45 | 
            +
                    '\n' => '\\n',
         | 
| 46 | 
            +
                    '\r' => '\\r',
         | 
| 47 | 
            +
                    '\\' => ' \\\\',
         | 
| 48 | 
            +
                    '"' => ' \\"',
         | 
| 49 | 
            +
                    "'" => " \\'",
         | 
| 50 | 
            +
                    '`' => ' \\`',
         | 
| 51 | 
            +
                    '$' => ' \\$'
         | 
| 52 | 
            +
                  }
         | 
| 45 53 | 
             
                  js_escape_map.each do |k, v|
         | 
| 46 54 | 
             
                    string = string.gsub(k, v)
         | 
| 47 55 | 
             
                  end
         | 
| 48 56 | 
             
                  string
         | 
| 49 57 | 
             
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def self.slugify(string)
         | 
| 60 | 
            +
                  (string.start_with?(/[0-9]/) ? 'slug-' : '') + string.downcase.gsub(' ', '-')
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def self.process(value)
         | 
| 64 | 
            +
                  klass = value.class
         | 
| 65 | 
            +
                  [Integer, Float].include?(klass) ? value : escape_javascript(value || '')
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                # limit width of special columns, that is, URL, Path, and Description
         | 
| 69 | 
            +
                # - data: array of arrays
         | 
| 70 | 
            +
                # - heading: array with column names
         | 
| 71 | 
            +
                # - width width to set
         | 
| 72 | 
            +
                def self.shorten(data, heading, width)
         | 
| 73 | 
            +
                  # indexes of columns which have to be shortened
         | 
| 74 | 
            +
                  keywords = %w[URL Referers Description Path]
         | 
| 75 | 
            +
                  to_shorten = keywords.map { |x| heading.index x }.compact
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  if width.nil? || to_shorten.empty? || data[0].nil?
         | 
| 78 | 
            +
                    data
         | 
| 79 | 
            +
                  else
         | 
| 80 | 
            +
                    table_columns = data[0].size
         | 
| 81 | 
            +
                    data.map { |x|
         | 
| 82 | 
            +
                      (0..table_columns - 1).each.map { |col|
         | 
| 83 | 
            +
                        should_shorten = x[col] && x[col].size > width - 3 && to_shorten.include?(col)
         | 
| 84 | 
            +
                        should_shorten ? "#{x[col][0..(width - 3)]}..." : x[col]
         | 
| 85 | 
            +
                      }
         | 
| 86 | 
            +
                    }
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                #
         | 
| 91 | 
            +
                # Specification of the reports to generate
         | 
| 92 | 
            +
                # Array of hashes with the following information:
         | 
| 93 | 
            +
                # - title: report_title
         | 
| 94 | 
            +
                #   header: header of tabular data
         | 
| 95 | 
            +
                #   rows: data to show
         | 
| 96 | 
            +
                #   column_alignment: specification of column alignments (works for txt reports)
         | 
| 97 | 
            +
                #   vega_spec: specifications for Vega output
         | 
| 98 | 
            +
                #   datatable_options: specific options for datatable
         | 
| 99 | 
            +
                def self.apache_report_specification(data = {})
         | 
| 100 | 
            +
                  [
         | 
| 101 | 
            +
                    { title: 'Daily Distribution',
         | 
| 102 | 
            +
                      header: %w[Day DOW Hits Visits Size],
         | 
| 103 | 
            +
                      column_alignment: %i[left left right right right],
         | 
| 104 | 
            +
                      rows: data[:daily_distribution],
         | 
| 105 | 
            +
                      vega_spec: {
         | 
| 106 | 
            +
                        'layer': [
         | 
| 107 | 
            +
                                   {
         | 
| 108 | 
            +
                                     'mark': {
         | 
| 109 | 
            +
                                               'type': 'line',
         | 
| 110 | 
            +
                                              'point': {
         | 
| 111 | 
            +
                                                         'filled': false,
         | 
| 112 | 
            +
                                                        'fill': 'white'
         | 
| 113 | 
            +
                                                       }
         | 
| 114 | 
            +
                                             },
         | 
| 115 | 
            +
                                    'encoding': {
         | 
| 116 | 
            +
                                                  'y': {'field': 'Hits', 'type': 'quantitative'}
         | 
| 117 | 
            +
                                                }
         | 
| 118 | 
            +
                                   },
         | 
| 119 | 
            +
                                   {
         | 
| 120 | 
            +
                                     'mark': {
         | 
| 121 | 
            +
                                               'type': 'text',
         | 
| 122 | 
            +
                                              'color': '#3E5772',
         | 
| 123 | 
            +
                                              'align': 'middle',
         | 
| 124 | 
            +
                                              'baseline': 'top',
         | 
| 125 | 
            +
                                              'dx': -10,
         | 
| 126 | 
            +
                                              'yOffset': -15
         | 
| 127 | 
            +
                                             },
         | 
| 128 | 
            +
                                    'encoding': {
         | 
| 129 | 
            +
                                                  'text': {'field': 'Hits', 'type': 'quantitative'},
         | 
| 130 | 
            +
                                                 'y': {'field': 'Hits', 'type': 'quantitative'}
         | 
| 131 | 
            +
                                                }
         | 
| 132 | 
            +
                                   },
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                                   {
         | 
| 135 | 
            +
                                     'mark': {
         | 
| 136 | 
            +
                                               'type': 'line',
         | 
| 137 | 
            +
                                              'color': '#A52A2A',
         | 
| 138 | 
            +
                                              'point': {
         | 
| 139 | 
            +
                                                         'color': '#A52A2A',
         | 
| 140 | 
            +
                                                        'filled': false,
         | 
| 141 | 
            +
                                                        'fill': 'white',
         | 
| 142 | 
            +
                                                       }
         | 
| 143 | 
            +
                                             },
         | 
| 144 | 
            +
                                    'encoding': {
         | 
| 145 | 
            +
                                                  'y': {'field': 'Visits', 'type': 'quantitative'}
         | 
| 146 | 
            +
                                                }
         | 
| 147 | 
            +
                                   },
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                                   {
         | 
| 150 | 
            +
                                     'mark': {
         | 
| 151 | 
            +
                                               'type': 'text',
         | 
| 152 | 
            +
                                              'color': '#A52A2A',
         | 
| 153 | 
            +
                                              'align': 'middle',
         | 
| 154 | 
            +
                                              'baseline': 'top',
         | 
| 155 | 
            +
                                              'dx': -10,
         | 
| 156 | 
            +
                                              'yOffset': -15
         | 
| 157 | 
            +
                                             },
         | 
| 158 | 
            +
                                    'encoding': {
         | 
| 159 | 
            +
                                                  'text': {'field': 'Visits', 'type': 'quantitative'},
         | 
| 160 | 
            +
                                                 'y': {'field': 'Visits', 'type': 'quantitative'}
         | 
| 161 | 
            +
                                                }
         | 
| 162 | 
            +
                                   },
         | 
| 163 | 
            +
                                   
         | 
| 164 | 
            +
                                 ],
         | 
| 165 | 
            +
                                  'encoding': {
         | 
| 166 | 
            +
                                                'x': {'field': 'Day', 'type': 'temporal'},
         | 
| 167 | 
            +
                                              }
         | 
| 168 | 
            +
                      }
         | 
| 169 | 
            +
                      
         | 
| 170 | 
            +
                    },
         | 
| 171 | 
            +
                    { title: 'Time Distribution',
         | 
| 172 | 
            +
                      header: %w[Hour Hits Visits Size],
         | 
| 173 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 174 | 
            +
                      rows: data[:time_distribution],
         | 
| 175 | 
            +
                      vega_spec: {
         | 
| 176 | 
            +
                        'layer': [
         | 
| 177 | 
            +
                                   {
         | 
| 178 | 
            +
                                     'mark': 'bar'
         | 
| 179 | 
            +
                                   },
         | 
| 180 | 
            +
                                   {
         | 
| 181 | 
            +
                                     'mark': {
         | 
| 182 | 
            +
                                               'type': 'text',
         | 
| 183 | 
            +
                                              'align': 'middle',
         | 
| 184 | 
            +
                                              'baseline': 'top',
         | 
| 185 | 
            +
                                              'dx': -10,
         | 
| 186 | 
            +
                                              'yOffset': -15
         | 
| 187 | 
            +
                                             },
         | 
| 188 | 
            +
                                    'encoding': {
         | 
| 189 | 
            +
                                                  'text': {'field': 'Hits', 'type': 'quantitative'},
         | 
| 190 | 
            +
                                                 'y': {'field': 'Hits', 'type': 'quantitative'}
         | 
| 191 | 
            +
                                                }
         | 
| 192 | 
            +
                                   },
         | 
| 193 | 
            +
                                 ],
         | 
| 194 | 
            +
                                  'encoding': {
         | 
| 195 | 
            +
                                                'x': {'field': 'Hour', 'type': 'nominal'},
         | 
| 196 | 
            +
                                               'y': {'field': 'Hits', 'type': 'quantitative'}
         | 
| 197 | 
            +
                                              }
         | 
| 198 | 
            +
                      }
         | 
| 199 | 
            +
                    },
         | 
| 200 | 
            +
                    {
         | 
| 201 | 
            +
                      title: '20_ and 30_ on HTML pages',
         | 
| 202 | 
            +
                      header: %w[Path Hits Visits Size Status],
         | 
| 203 | 
            +
                      column_alignment: %i[left right right right right],
         | 
| 204 | 
            +
                      rows: data[:most_requested_pages],
         | 
| 205 | 
            +
                      datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
         | 
| 206 | 
            +
                    },
         | 
| 207 | 
            +
                    {
         | 
| 208 | 
            +
                      title: '20_ and 30_ on other resources',
         | 
| 209 | 
            +
                      header: %w[Path Hits Visits Size Status],
         | 
| 210 | 
            +
                      column_alignment: %i[left right right right right],
         | 
| 211 | 
            +
                      rows: data[:most_requested_resources],
         | 
| 212 | 
            +
                      datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
         | 
| 213 | 
            +
                    },
         | 
| 214 | 
            +
                    {
         | 
| 215 | 
            +
                      title: '40_ and 50_x on HTML pages',
         | 
| 216 | 
            +
                      header: %w[Path Hits Visits Status],
         | 
| 217 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 218 | 
            +
                      rows: data[:missed_pages],
         | 
| 219 | 
            +
                      datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
         | 
| 220 | 
            +
                    },
         | 
| 221 | 
            +
                    {
         | 
| 222 | 
            +
                      title: '40_ and 50_ on other resources',
         | 
| 223 | 
            +
                      header: %w[Path Hits Visits Status],
         | 
| 224 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 225 | 
            +
                      rows: data[:missed_resources],
         | 
| 226 | 
            +
                      datatable_options: 'columnDefs: [{ width: \'40%\', targets: 0 } ]'
         | 
| 227 | 
            +
                    },
         | 
| 228 | 
            +
                    {
         | 
| 229 | 
            +
                      title: 'Statuses',
         | 
| 230 | 
            +
                      header: %w[Status Count],
         | 
| 231 | 
            +
                      column_alignment: %i[left right],
         | 
| 232 | 
            +
                      rows: data[:statuses],
         | 
| 233 | 
            +
                      vega_spec: {
         | 
| 234 | 
            +
                        'mark': 'bar',
         | 
| 235 | 
            +
                                  'encoding': {
         | 
| 236 | 
            +
                                                'x': {'field': 'Status', 'type': 'nominal'},
         | 
| 237 | 
            +
                                               'y': {'field': 'Count', 'type': 'quantitative'}
         | 
| 238 | 
            +
                                              }
         | 
| 239 | 
            +
                      }
         | 
| 240 | 
            +
                    },
         | 
| 241 | 
            +
                    {
         | 
| 242 | 
            +
                      title: 'Daily Statuses',
         | 
| 243 | 
            +
                      header: %w[Date S_2xx S_3xx S_4xx],
         | 
| 244 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 245 | 
            +
                      rows: data[:statuses_by_day],
         | 
| 246 | 
            +
                      vega_spec: {
         | 
| 247 | 
            +
                        'transform': [ {'fold': ['S_2xx', 'S_3xx', 'S_4xx' ] }],
         | 
| 248 | 
            +
                                  'mark': 'bar',
         | 
| 249 | 
            +
                                  'encoding': {
         | 
| 250 | 
            +
                                                'x': {
         | 
| 251 | 
            +
                                                       'field': 'Date',
         | 
| 252 | 
            +
                                                      'type': 'ordinal',
         | 
| 253 | 
            +
                                                      'timeUnit': 'day', 
         | 
| 254 | 
            +
                                                     },
         | 
| 255 | 
            +
                                               'y': {
         | 
| 256 | 
            +
                                                      'aggregate': 'sum',
         | 
| 257 | 
            +
                                                     'field': 'value',
         | 
| 258 | 
            +
                                                     'type': 'quantitative'
         | 
| 259 | 
            +
                                                    },
         | 
| 260 | 
            +
                                               'color': {
         | 
| 261 | 
            +
                                                          'field': 'key',
         | 
| 262 | 
            +
                                                         'type': 'nominal',
         | 
| 263 | 
            +
                                                         'scale': {
         | 
| 264 | 
            +
                                                                    'domain': ['S_2xx', 'S_3xx', 'S_4xx'],
         | 
| 265 | 
            +
                                                                   'range': ['#228b22', '#ff8c00', '#a52a2a']
         | 
| 266 | 
            +
                                                                  },
         | 
| 267 | 
            +
                                                        }
         | 
| 268 | 
            +
                                              }
         | 
| 269 | 
            +
                      }
         | 
| 270 | 
            +
                    },
         | 
| 271 | 
            +
                    { title: 'Browsers',
         | 
| 272 | 
            +
                      header: %w[Browser Hits Visits Size],
         | 
| 273 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 274 | 
            +
                      rows: data[:browsers],
         | 
| 275 | 
            +
                      vega_spec: {
         | 
| 276 | 
            +
                        'layer': [
         | 
| 277 | 
            +
                                   { 'mark': 'bar' },
         | 
| 278 | 
            +
                                   {
         | 
| 279 | 
            +
                                     'mark': {
         | 
| 280 | 
            +
                                               'type': 'text',
         | 
| 281 | 
            +
                                              'align': 'middle',
         | 
| 282 | 
            +
                                              'baseline': 'top',
         | 
| 283 | 
            +
                                              'dx': -10,
         | 
| 284 | 
            +
                                              'yOffset': -15
         | 
| 285 | 
            +
                                             },
         | 
| 286 | 
            +
                                    'encoding': {
         | 
| 287 | 
            +
                                                  'text': {'field': 'Hits', 'type': 'quantitative'},
         | 
| 288 | 
            +
                                                }
         | 
| 289 | 
            +
                                   },
         | 
| 290 | 
            +
                                 ],
         | 
| 291 | 
            +
                                  'encoding': {
         | 
| 292 | 
            +
                                                'x': {'field': 'Browser', 'type': 'nominal'},
         | 
| 293 | 
            +
                                               'y': {'field': 'Hits', 'type': 'quantitative'}
         | 
| 294 | 
            +
                                              }
         | 
| 295 | 
            +
                      }
         | 
| 296 | 
            +
                    },
         | 
| 297 | 
            +
                    { title: 'Platforms',
         | 
| 298 | 
            +
                      header: %w[Platform Hits Visits Size],
         | 
| 299 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 300 | 
            +
                      rows: data[:platforms],
         | 
| 301 | 
            +
                      vega_spec: {
         | 
| 302 | 
            +
                        'layer': [
         | 
| 303 | 
            +
                                   { 'mark': 'bar' },
         | 
| 304 | 
            +
                                   {
         | 
| 305 | 
            +
                                     'mark': {
         | 
| 306 | 
            +
                                               'type': 'text',
         | 
| 307 | 
            +
                                              'align': 'middle',
         | 
| 308 | 
            +
                                              'baseline': 'top',
         | 
| 309 | 
            +
                                              'dx': -10,
         | 
| 310 | 
            +
                                              'yOffset': -15
         | 
| 311 | 
            +
                                             },
         | 
| 312 | 
            +
                                    'encoding': {
         | 
| 313 | 
            +
                                                  'text': {'field': 'Hits', 'type': 'quantitative'},
         | 
| 314 | 
            +
                                                }
         | 
| 315 | 
            +
                                   },
         | 
| 316 | 
            +
                                 ],
         | 
| 317 | 
            +
                                  'encoding': {
         | 
| 318 | 
            +
                                                'x': {'field': 'Platform', 'type': 'nominal'},
         | 
| 319 | 
            +
                                               'y': {'field': 'Hits', 'type': 'quantitative'}
         | 
| 320 | 
            +
                                              }
         | 
| 321 | 
            +
                      }
         | 
| 322 | 
            +
                    },
         | 
| 323 | 
            +
                    {
         | 
| 324 | 
            +
                      title: 'IPs',
         | 
| 325 | 
            +
                      header: %w[IPs Hits Visits Size Country],
         | 
| 326 | 
            +
                      column_alignment: %i[left right right right left],
         | 
| 327 | 
            +
                      rows: data[:ips]
         | 
| 328 | 
            +
                    },
         | 
| 329 | 
            +
                    {
         | 
| 330 | 
            +
                      title: 'Countries',
         | 
| 331 | 
            +
                      header: %w[Country Hits Visits IPs],
         | 
| 332 | 
            +
                      column_alignment: %i[left right right left],
         | 
| 333 | 
            +
                      rows: data[:countries]&.map do |k, v|
         | 
| 334 | 
            +
                        [
         | 
| 335 | 
            +
                          k,
         | 
| 336 | 
            +
                          v.map { |x| x[1] }.inject(&:+),
         | 
| 337 | 
            +
                          v.map { |x| x[2] }.inject(&:+),
         | 
| 338 | 
            +
                          v.map { |x| x[0] }.join(' ')
         | 
| 339 | 
            +
                        ]
         | 
| 340 | 
            +
                      end
         | 
| 341 | 
            +
                    },
         | 
| 342 | 
            +
                    {
         | 
| 343 | 
            +
                      title: 'Referers',
         | 
| 344 | 
            +
                      header: %w[Referers Hits Visits Size],
         | 
| 345 | 
            +
                      column_alignment: %i[left right right right],
         | 
| 346 | 
            +
                      rows: data[:referers],
         | 
| 347 | 
            +
                      col: 'small-12 cell'
         | 
| 348 | 
            +
                    },
         | 
| 349 | 
            +
                    {
         | 
| 350 | 
            +
                      title: 'Streaks',
         | 
| 351 | 
            +
                      report: :html,
         | 
| 352 | 
            +
                      header: ['IP', 'Date', 'Total HTML', 'Total Other', 'HTML', 'Other'],
         | 
| 353 | 
            +
                      column_alignment: %i[left left right right left left],
         | 
| 354 | 
            +
                      rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
         | 
| 355 | 
            +
                        [
         | 
| 356 | 
            +
                          k[0],
         | 
| 357 | 
            +
                          k[1],
         | 
| 358 | 
            +
                          v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.size,
         | 
| 359 | 
            +
                          v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.size,
         | 
| 360 | 
            +
                          v.map { |x| x[2] }.compact.select { |x| x.match(/\.html?$/) }.join(' ■ '),
         | 
| 361 | 
            +
                          v.map { |x| x[2] }.compact.reject { |x| x.match(/\.html?$/) }.join(' ■ ')
         | 
| 362 | 
            +
                        ]
         | 
| 363 | 
            +
                      end,
         | 
| 364 | 
            +
                      col: 'small-12 cell'
         | 
| 365 | 
            +
                    }
         | 
| 366 | 
            +
                  ]
         | 
| 367 | 
            +
                end
         | 
| 368 | 
            +
             | 
| 369 | 
            +
                def self.rails_report_specification(data = {})
         | 
| 370 | 
            +
                  [
         | 
| 371 | 
            +
                    {
         | 
| 372 | 
            +
                      title: "Daily Distribution",
         | 
| 373 | 
            +
                      header: %w[Day DOW Hits],
         | 
| 374 | 
            +
                      column_alignment: %i[left left right],
         | 
| 375 | 
            +
                      rows: data[:daily_distribution],
         | 
| 376 | 
            +
                      vega_spec: {
         | 
| 377 | 
            +
                        "encoding": {
         | 
| 378 | 
            +
                                      "x": {"field": "Day", "type": "temporal"},
         | 
| 379 | 
            +
                                     "y": {"field": "Hits", "type": "quantitative"}
         | 
| 380 | 
            +
                                    },
         | 
| 381 | 
            +
                                  "layer": [
         | 
| 382 | 
            +
                                             {
         | 
| 383 | 
            +
                                               "mark": {
         | 
| 384 | 
            +
                                                         "type": "line",
         | 
| 385 | 
            +
                                                        "point": {
         | 
| 386 | 
            +
                                                                   "filled": false,
         | 
| 387 | 
            +
                                                                  "fill": "white"
         | 
| 388 | 
            +
                                                                 }
         | 
| 389 | 
            +
                                                       }
         | 
| 390 | 
            +
                                             },
         | 
| 391 | 
            +
                                             {
         | 
| 392 | 
            +
                                               "mark": {
         | 
| 393 | 
            +
                                                         "type": "text",
         | 
| 394 | 
            +
                                                        "align": "left",
         | 
| 395 | 
            +
                                                        "baseline": "middle",
         | 
| 396 | 
            +
                                                        "dx": 5
         | 
| 397 | 
            +
                                                       },
         | 
| 398 | 
            +
                                              "encoding": {
         | 
| 399 | 
            +
                                                            "text": {"field": "Hits", "type": "quantitative"}
         | 
| 400 | 
            +
                                                          }
         | 
| 401 | 
            +
                                             }
         | 
| 402 | 
            +
                                           ]
         | 
| 403 | 
            +
                      }
         | 
| 404 | 
            +
                    },
         | 
| 405 | 
            +
                    {
         | 
| 406 | 
            +
                      title: "Time Distribution",
         | 
| 407 | 
            +
                      header: %w[Hour Hits],
         | 
| 408 | 
            +
                      column_alignment: %i[left right],
         | 
| 409 | 
            +
                      rows: data[:time_distribution],
         | 
| 410 | 
            +
                      vega_spec: {
         | 
| 411 | 
            +
                        "layer": [
         | 
| 412 | 
            +
                                   {
         | 
| 413 | 
            +
                                     "mark": "bar",
         | 
| 414 | 
            +
                                   },
         | 
| 415 | 
            +
                                   {
         | 
| 416 | 
            +
                                     "mark": {
         | 
| 417 | 
            +
                                               "type": "text",
         | 
| 418 | 
            +
                                              "align": "middle",
         | 
| 419 | 
            +
                                              "baseline": "top",
         | 
| 420 | 
            +
                                              "dx": -10,
         | 
| 421 | 
            +
                                              "yOffset": -15
         | 
| 422 | 
            +
                                             },
         | 
| 423 | 
            +
                                    "encoding": {
         | 
| 424 | 
            +
                                                  "text": {"field": "Hits", "type": "quantitative"}
         | 
| 425 | 
            +
                                                }
         | 
| 426 | 
            +
                                   }
         | 
| 427 | 
            +
                                 ],
         | 
| 428 | 
            +
                                  "encoding": {
         | 
| 429 | 
            +
                                                "x": {"field": "Hour", "type": "nominal"},
         | 
| 430 | 
            +
                                               "y": {"field": "Hits", "type": "quantitative"}
         | 
| 431 | 
            +
                                              }
         | 
| 432 | 
            +
                      }
         | 
| 433 | 
            +
                    },
         | 
| 434 | 
            +
                    {
         | 
| 435 | 
            +
                      title: "Statuses",
         | 
| 436 | 
            +
                      header: %w[Status Count],
         | 
| 437 | 
            +
                      column_alignment: %i[left right],
         | 
| 438 | 
            +
                      rows: data[:statuses],
         | 
| 439 | 
            +
                      vega_spec: {
         | 
| 440 | 
            +
                        "layer": [
         | 
| 441 | 
            +
                                   {
         | 
| 442 | 
            +
                                     "mark": "bar"
         | 
| 443 | 
            +
                                   },
         | 
| 444 | 
            +
                                   {
         | 
| 445 | 
            +
                                     "mark": {
         | 
| 446 | 
            +
                                               "type": "text",
         | 
| 447 | 
            +
                                              "align": "left",
         | 
| 448 | 
            +
                                              "baseline": "top",
         | 
| 449 | 
            +
                                              "dx": -10,
         | 
| 450 | 
            +
                                              "yOffset": -20
         | 
| 451 | 
            +
                                             },
         | 
| 452 | 
            +
                                    "encoding": {
         | 
| 453 | 
            +
                                                  "text": {"field": "Count", "type": "quantitative"}
         | 
| 454 | 
            +
                                                }
         | 
| 455 | 
            +
                                   }
         | 
| 456 | 
            +
                                 ],
         | 
| 457 | 
            +
                                  "encoding": {
         | 
| 458 | 
            +
                                                "x": {"field": "Status", "type": "nominal"},
         | 
| 459 | 
            +
                                               "y": {"field": "Count", "type": "quantitative"}
         | 
| 460 | 
            +
                                              }
         | 
| 461 | 
            +
                      }
         | 
| 462 | 
            +
                    },
         | 
| 463 | 
            +
                    {
         | 
| 464 | 
            +
                      title: "Rails Performance",
         | 
| 465 | 
            +
                      header: %w[Controller Hits Min Avg Max],
         | 
| 466 | 
            +
                      column_alignment: %i[left right right right right],
         | 
| 467 | 
            +
                      rows: data[:performance],
         | 
| 468 | 
            +
                      vega_spec: {
         | 
| 469 | 
            +
                        "layer": [
         | 
| 470 | 
            +
                                   {
         | 
| 471 | 
            +
                                     "mark": {
         | 
| 472 | 
            +
                                               "type": "point",
         | 
| 473 | 
            +
                                              "name": "data_points"
         | 
| 474 | 
            +
                                             }
         | 
| 475 | 
            +
                                   },
         | 
| 476 | 
            +
                                   {
         | 
| 477 | 
            +
                                     "mark": {
         | 
| 478 | 
            +
                                               "name": "label",
         | 
| 479 | 
            +
                                              "type": "text",
         | 
| 480 | 
            +
                                              "align": "left",
         | 
| 481 | 
            +
                                              "baseline": "middle",
         | 
| 482 | 
            +
                                              "dx": 5,
         | 
| 483 | 
            +
                                              "yOffset": 0
         | 
| 484 | 
            +
                                             },
         | 
| 485 | 
            +
                                    "encoding": {
         | 
| 486 | 
            +
                                                  "text": {"field": "Controller"},
         | 
| 487 | 
            +
                                                 "fontSize": {"value": 8}
         | 
| 488 | 
            +
                                                },
         | 
| 489 | 
            +
                                   },
         | 
| 490 | 
            +
                                 ],
         | 
| 491 | 
            +
                                  "encoding": {
         | 
| 492 | 
            +
                                                "x": {"field": "Avg", "type": "quantitative"},
         | 
| 493 | 
            +
                                               "y": {"field": "Hits", "type": "quantitative"}
         | 
| 494 | 
            +
                                              },
         | 
| 495 | 
            +
                      }
         | 
| 496 | 
            +
                    },
         | 
| 497 | 
            +
                    {
         | 
| 498 | 
            +
                      title: "Fatal Events",
         | 
| 499 | 
            +
                      header: %w[Date IP URL Description Log ID],
         | 
| 500 | 
            +
                      column_alignment: %i[left left left left left],
         | 
| 501 | 
            +
                      rows: data[:fatal],
         | 
| 502 | 
            +
                      col: 'small-12 cell'
         | 
| 503 | 
            +
                    },
         | 
| 504 | 
            +
                    {
         | 
| 505 | 
            +
                      title: 'Internal Server Errors',
         | 
| 506 | 
            +
                      header: %w[Date Status IP URL Description Log ID],
         | 
| 507 | 
            +
                      column_alignment: %i[left left left left left left],
         | 
| 508 | 
            +
                      rows: data[:internal_server_error],
         | 
| 509 | 
            +
                      col: 'small-12 cell'
         | 
| 510 | 
            +
                    },
         | 
| 511 | 
            +
                    {
         | 
| 512 | 
            +
                      title: 'Errors',
         | 
| 513 | 
            +
                      header: %w[Log ID Context Description Count],
         | 
| 514 | 
            +
                      column_alignment: %i[left left left left],
         | 
| 515 | 
            +
                      rows: data[:error],
         | 
| 516 | 
            +
                      col: 'small-12 cell'
         | 
| 517 | 
            +
                    },
         | 
| 518 | 
            +
                    {
         | 
| 519 | 
            +
                      title: 'IPs',
         | 
| 520 | 
            +
                      header: %w[IPs Hits Country],
         | 
| 521 | 
            +
                      column_alignment: %i[left right left],
         | 
| 522 | 
            +
                      rows: data[:ips]
         | 
| 523 | 
            +
                    },
         | 
| 524 | 
            +
                    {
         | 
| 525 | 
            +
                      title: 'Countries',
         | 
| 526 | 
            +
                      header: %w[Country Hits IPs],
         | 
| 527 | 
            +
                      column_alignment: %i[left right left],
         | 
| 528 | 
            +
                      rows: data[:countries]&.map do |k, v|
         | 
| 529 | 
            +
                        [
         | 
| 530 | 
            +
                          k,
         | 
| 531 | 
            +
                          v.map { |x| x[1] }.inject(&:+),
         | 
| 532 | 
            +
                          v.map { |x| x[0] }.join(' ■ ')
         | 
| 533 | 
            +
                        ]
         | 
| 534 | 
            +
                      end
         | 
| 535 | 
            +
                    },
         | 
| 536 | 
            +
                    {
         | 
| 537 | 
            +
                      title: 'Streaks',
         | 
| 538 | 
            +
                      report: :html,
         | 
| 539 | 
            +
                      header: %w[IP Date Total Resources],
         | 
| 540 | 
            +
                      column_alignment: %i[left left right right left left],
         | 
| 541 | 
            +
                      rows: data[:streaks]&.group_by { |x| [x[0], x[1]] }&.map do |k, v|
         | 
| 542 | 
            +
                        [
         | 
| 543 | 
            +
                          k[0],
         | 
| 544 | 
            +
                          k[1],
         | 
| 545 | 
            +
                          v.size,
         | 
| 546 | 
            +
                          v.map { |x| x[2] }.join(' ■ ')
         | 
| 547 | 
            +
                        ]
         | 
| 548 | 
            +
                      end,
         | 
| 549 | 
            +
                      col: 'small-12 cell'
         | 
| 550 | 
            +
                    }
         | 
| 551 | 
            +
                  ]
         | 
| 552 | 
            +
                end
         | 
| 50 553 | 
             
              end
         | 
| 51 554 | 
             
            end
         | 
    
        data/lib/log_sense/ip_locator.rb
    CHANGED
    
    | @@ -4,19 +4,22 @@ require 'ipaddr' | |
| 4 4 | 
             
            require 'iso_country_codes'
         | 
| 5 5 |  | 
| 6 6 | 
             
            module LogSense
         | 
| 7 | 
            +
              #
         | 
| 8 | 
            +
              # Populate table of IP Locations from dbip-country-lite
         | 
| 9 | 
            +
              #
         | 
| 7 10 | 
             
              module IpLocator
         | 
| 8 | 
            -
                DB_FILE = File.join(File.dirname(__FILE__),  | 
| 11 | 
            +
                DB_FILE = File.join(File.dirname(__FILE__), '..', '..', 'ip_locations', 'dbip-country-lite.sqlite3')
         | 
| 9 12 |  | 
| 10 | 
            -
                def self.dbip_to_sqlite | 
| 11 | 
            -
                  db = SQLite3::Database.new  | 
| 12 | 
            -
                  db.execute  | 
| 13 | 
            +
                def self.dbip_to_sqlite(db_location)
         | 
| 14 | 
            +
                  db = SQLite3::Database.new ':memory:'
         | 
| 15 | 
            +
                  db.execute 'CREATE TABLE ip_location (
         | 
| 13 16 | 
             
                    from_ip_n INTEGER,
         | 
| 14 17 | 
             
                    from_ip TEXT,
         | 
| 15 18 | 
             
                    to_ip TEXT,
         | 
| 16 19 | 
             
                    country_code TEXT
         | 
| 17 | 
            -
                  ) | 
| 20 | 
            +
                  )'
         | 
| 18 21 |  | 
| 19 | 
            -
                  ins = db.prepare  | 
| 22 | 
            +
                  ins = db.prepare 'INSERT INTO ip_location(from_ip_n, from_ip, to_ip, country_code) values (?, ?, ?, ?)'
         | 
| 20 23 | 
             
                  CSV.foreach(db_location) do |row|
         | 
| 21 24 | 
             
                    ip = IPAddr.new row[0]
         | 
| 22 25 | 
             
                    ins.execute(ip.to_i, row[0], row[1], row[2])
         | 
| @@ -33,29 +36,33 @@ module LogSense | |
| 33 36 | 
             
                  SQLite3::Database.new DB_FILE
         | 
| 34 37 | 
             
                end
         | 
| 35 38 |  | 
| 36 | 
            -
                def self.locate_ip | 
| 37 | 
            -
                  return  | 
| 39 | 
            +
                def self.locate_ip(ip, db)
         | 
| 40 | 
            +
                  return unless ip
         | 
| 38 41 |  | 
| 39 | 
            -
                   | 
| 40 | 
            -
                  res = db.execute "SELECT * FROM ip_location where from_ip_n <= #{ip_n} order by from_ip_n desc limit 1"
         | 
| 42 | 
            +
                  query = db.prepare 'SELECT * FROM ip_location where from_ip_n <= ? order by from_ip_n desc limit 1'
         | 
| 41 43 | 
             
                  begin
         | 
| 42 | 
            -
                     | 
| 43 | 
            -
             | 
| 44 | 
            -
                     | 
| 44 | 
            +
                    ip_n = IPAddr.new(ip).to_i
         | 
| 45 | 
            +
                    result_set = query.execute ip_n
         | 
| 46 | 
            +
                    country_code = result_set.map { |x| x[3] }[0]
         | 
| 47 | 
            +
                    IsoCountryCodes.find(country_code).name
         | 
| 48 | 
            +
                  rescue IPAddr::InvalidAddressError
         | 
| 49 | 
            +
                    'INVALID IP'
         | 
| 50 | 
            +
                  rescue IsoCountryCodes::UnknownCodeError
         | 
| 51 | 
            +
                    country_code
         | 
| 45 52 | 
             
                  end
         | 
| 46 53 | 
             
                end
         | 
| 47 54 |  | 
| 48 55 | 
             
                #
         | 
| 49 56 | 
             
                # add country code to data[:ips]
         | 
| 50 57 | 
             
                #
         | 
| 51 | 
            -
                def self.geolocate | 
| 52 | 
            -
                  @location_db = IpLocator | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
                     | 
| 58 | 
            +
                def self.geolocate(data)
         | 
| 59 | 
            +
                  @location_db = IpLocator.load_db
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  data[:ips].each do |line|
         | 
| 62 | 
            +
                    country_code = IpLocator.locate_ip line[0], @location_db
         | 
| 63 | 
            +
                    line << country_code
         | 
| 56 64 | 
             
                  end
         | 
| 57 65 | 
             
                  data
         | 
| 58 66 | 
             
                end
         | 
| 59 | 
            -
             | 
| 60 67 | 
             
              end
         | 
| 61 68 | 
             
            end
         |