log_sense 2.3.1 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43c38b704af22a839fedec504755be79abf4307c1183efe65ee7323984c4d6bc
4
- data.tar.gz: ed1b73c4b9ccc5e6ffcae7682e67124aa82dba1d9fde9ab625764207893a11dd
3
+ metadata.gz: 85b0ebee3c5879005dce7f3e32675a5f0319411eb74f138b2dfd7a4c21d26d43
4
+ data.tar.gz: 1aec3ca221196cfaf9061495e6767f5136a23a1f9d6f510fb43a28c056c1ca8c
5
5
  SHA512:
6
- metadata.gz: 19857e10b0f5b72afdb491245101050a3348e267d6a9409416ab478e72b441ed8b559280f492f1c761753cbfd71abdafd5184f60095c53ca1bc0640a83c22f4a
7
- data.tar.gz: 3f049b2b6756bfe9ef872021f6b34263736b12eb75ec2b06f086387550fbd87efe367ccc4348070cb492f5870b286c626fe6244ab19ea01f74684d301175b32b
6
+ metadata.gz: 27389b84f365fdaa02ece44dd70eabb130051ca81d26d17cd573325f8101d21c0dc83fd5c81915babfcd1e8d1f363ef777811f9676b2439e8f6b868013515700
7
+ data.tar.gz: e52b3e05fb838362c3938aaf20d1f6347a3a4c8025d40963c98eeae4fe4b6c01d9acf225353ce2c872c213b2f113af089de418bc0e424e4b151884f4fd37c03c
data/.gitignore CHANGED
@@ -8,3 +8,6 @@
8
8
  /tmp/
9
9
  node_modules
10
10
  *~
11
+ TAGS
12
+ sample_logs
13
+ ip_locations/dbip-country-lite-*.csv*
data/CHANGELOG.org CHANGED
@@ -2,6 +2,16 @@
2
2
  #+AUTHOR: Adolfo Villafiorita
3
3
  #+STARTUP: showall
4
4
 
5
+ * 2.5.0
6
+
7
+ - Add query reports
8
+
9
+ * 2.4.0
10
+
11
+ - Updates log_parser to match changes in log lines introduced from Rails 7
12
+ (or, in other words, fixes a bug which caused log_sense to miss events
13
+ from logs produced by Rails >= 7 applications)
14
+
5
15
  * 2.3.1
6
16
 
7
17
  - Updates geoip db
data/Gemfile.lock CHANGED
@@ -3,8 +3,10 @@ PATH
3
3
  specs:
4
4
  log_sense (2.3.1)
5
5
  browser (~> 6.0.0)
6
+ csv
6
7
  ipaddr (~> 1.2.0)
7
8
  iso_country_codes (~> 0.7.0)
9
+ ostruct
8
10
  sqlite3 (~> 2.6.0)
9
11
  terminal-table (~> 4.0.0)
10
12
 
@@ -18,6 +20,7 @@ GEM
18
20
  bundler-audit (0.9.2)
19
21
  bundler (>= 1.2.0, < 3)
20
22
  thor (~> 1.0)
23
+ csv (3.3.4)
21
24
  date (3.4.1)
22
25
  debug (1.10.0)
23
26
  irb (~> 1.10)
@@ -34,6 +37,7 @@ GEM
34
37
  lint_roller (1.1.0)
35
38
  mini_portile2 (2.8.9)
36
39
  minitest (5.25.5)
40
+ ostruct (0.6.1)
37
41
  parallel (1.27.0)
38
42
  parser (3.3.8.0)
39
43
  ast (~> 2.4.1)
data/README.org CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  * Introduction
6
6
 
7
- LogSense generates reports and statistics from Ruby on Rails and Apache/Nginx
8
- log files.
7
+ LogSense analyzes Apache and Rails log files, generating reports and statistics
8
+ about usage and performace.
9
9
 
10
10
  Main features:
11
11
 
@@ -22,10 +22,11 @@ Main features:
22
22
  self polls and crawlers.
23
23
  - Reports can be generated in HTML, txt, ufw, and SQLite. HTML reports are
24
24
  responsive and come with dark and light theme.
25
+ - It works with Rails' vanilla log format (no need to format the log)
25
26
 
26
27
  LogSense is Written in Ruby, it runs from the command line, it is fast, and it
27
28
  can be installed on any system with a relatively recent version of Ruby. We
28
- use it with Ruby 3.1.4 and 3.3.0.
29
+ started using it with Ruby 3.1.4 and we are now at version 3.4.
29
30
 
30
31
  It is fast. On a ThinkPad P16, a 277M log file is parsed in 15 seconds,
31
32
  processing, that is, about 7740 events per second; a 569M log file is parsed in
@@ -16,50 +16,43 @@ module LogSense
16
16
  def parse(streams, options = {})
17
17
  db = SQLite3::Database.new ":memory:"
18
18
 
19
+ event_table = {
20
+ exit_status: "TEXT",
21
+ started_at: "TEXT",
22
+ ended_at: "TEXT",
23
+ log_id: "TEXT",
24
+ ip: "TEXT",
25
+ unique_visitor: "TEXT",
26
+ url: "TEXT",
27
+ controller: "TEXT",
28
+ html_verb: "TEXT",
29
+ status: "INTEGER",
30
+ duration_total_ms: "FLOAT",
31
+ duration_views_ms: "FLOAT",
32
+ duration_ar_ms: "FLOAT",
33
+ allocations: "INTEGER",
34
+ queries: "INTEGER",
35
+ cached_queries: "INTEGER",
36
+ gc_duration: "FLOAT",
37
+ comment: "TEXT",
38
+ source_file: "TEXT",
39
+ line_number: "INTEGER"
40
+ }
41
+
42
+ table_declaration = event_table.map { |k, v| "#{k} #{v}" }.join(",")
19
43
  db.execute <<-EOS
20
- CREATE TABLE IF NOT EXISTS Event(
21
- id INTEGER PRIMARY KEY AUTOINCREMENT,
22
- exit_status TEXT,
23
- started_at TEXT,
24
- ended_at TEXT,
25
- log_id TEXT,
26
- ip TEXT,
27
- unique_visitor TEXT,
28
- url TEXT,
29
- controller TEXT,
30
- html_verb TEXT,
31
- status INTEGER,
32
- duration_total_ms FLOAT,
33
- duration_views_ms FLOAT,
34
- duration_ar_ms FLOAT,
35
- allocations INTEGER,
36
- comment TEXT,
37
- source_file TEXT,
38
- line_number INTEGER
39
- )
44
+ CREATE TABLE IF NOT EXISTS Event(
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ #{table_declaration}
47
+ )
40
48
  EOS
41
49
 
50
+ tds = event_table.keys.size
42
51
  ins = db.prepare <<-EOS
43
- insert into Event(
44
- exit_status,
45
- started_at,
46
- ended_at,
47
- log_id,
48
- ip,
49
- unique_visitor,
50
- url,
51
- controller,
52
- html_verb,
53
- status,
54
- duration_total_ms,
55
- duration_views_ms,
56
- duration_ar_ms,
57
- allocations,
58
- comment,
59
- source_file,
60
- line_number
52
+ insert into Event(
53
+ #{event_table.keys.join(", ")}
61
54
  )
62
- values (#{Array.new(17, "?").join(", ")})
55
+ values (#{Array.new(tds, "?").join(", ")})
63
56
  EOS
64
57
 
65
58
  db.execute <<-EOS
@@ -180,9 +173,16 @@ module LogSense
180
173
  # LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Processing by CONTROLLER as FORMAT
181
174
  # LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Parameters: JSON
182
175
  # LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Rendered VIEW within LAYOUT (Duration: DURATION | Allocations: ALLOCATIONS)
176
+ #
177
+ # For rails_6:
178
+ #
183
179
  # LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Completed STATUS STATUS_STRING in DURATION (Views: DURATION | ActiveRecord: DURATION | Allocations: NUMBER)
184
180
  #
185
- # and they appears in the order shown above: started, processing, ...
181
+ # For rails_7:
182
+ #
183
+ # LOG_LEVEL, [ZULU_TIMESTAMP #NUMBER] INFO --: [ID] Completed STATUS STATUS_STRING in DURATION (Views: DURATION | ActiveRecord: DURATION (N queries, M cached) | GC: DURATION)
184
+ #
185
+ # and they appear in the order shown above: started, processing, ...
186
186
  #
187
187
  # Different requests might be interleaved, of course
188
188
  #
@@ -244,6 +244,9 @@ module LogSense
244
244
  event[:duration_views_ms],
245
245
  event[:duration_ar_ms],
246
246
  event[:allocations],
247
+ event[:queries],
248
+ event[:cached_queries],
249
+ event[:gc_duration],
247
250
  event[:comment],
248
251
  filename,
249
252
  line_number
@@ -280,6 +283,9 @@ module LogSense
280
283
  event[:duration_views_ms],
281
284
  event[:duration_ar_ms],
282
285
  event[:allocations],
286
+ event[:queries],
287
+ event[:cached_queries],
288
+ event[:gc_duration],
283
289
  event[:comment],
284
290
  filename,
285
291
  line_number
@@ -434,6 +440,7 @@ module LogSense
434
440
  STATUS = '(?<status>[0-9]+)'
435
441
  STATUS_IN_WORDS = '(OK|Unauthorized|Found|Internal Server Error|Bad Request|Method Not Allowed|Request Timeout|Not Implemented|Bad Gateway|Service Unavailable)'
436
442
  MSECS = '[0-9.]+'
443
+ INT = '[0-9]+'
437
444
 
438
445
  # I, [2021-10-19T08:16:34.343858 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Started GET "/grow/people/471" for 217.77.80.35 at 2021-10-19 08:16:34 +0000
439
446
  STARTED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] Started #{VERB} "#{URL}" for #{IP} at/o
@@ -455,7 +462,21 @@ module LogSense
455
462
  # I, [2021-10-19T08:16:34.712331 #10477] INFO -- : [67103c0d-455d-4fe8-951e-87e97628cb66] Completed 200 OK in 367ms (Views: 216.7ms | ActiveRecord: 141.3ms | Allocations: 168792)
456
463
  # I, [2021-12-09T16:53:52.657727 #2735058] INFO -- : [0064e403-9eb2-439d-8fe1-a334c86f5532] Completed 200 OK in 13ms (Views: 11.1ms | ActiveRecord: 1.2ms)
457
464
  # 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)
458
- COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \((Views: (?<views>#{MSECS})ms \| )?ActiveRecord: (?<arec>#{MSECS})ms( \| Allocations: (?<alloc>[0-9]+))?\)/o
465
+
466
+ # REGEXP Fragments
467
+
468
+ # views is optional
469
+ # In strings I need to properly escape \ or it will be mis-interpreted
470
+ VIEWS = "(Views: (?<views>#{MSECS})ms \\| )?"
471
+ # active records has two formats (according to Rails version)
472
+ ACTIVE_RECORDS_BASE = "ActiveRecord: (?<arec>#{MSECS})ms"
473
+ ACTIVE_RECORDS_QUERIES = "( \\((?<queries>#{INT}) queries, (?<cached_queries>#{INT}) cached\\))?"
474
+ ACTIVE_RECORDS = "#{ACTIVE_RECORDS_BASE}#{ACTIVE_RECORDS_QUERIES}"
475
+ # older rails versions have ALLOCATIONS, newer have GC
476
+ ALLOCATIONS = " \\| Allocations: (?<alloc>#{INT})"
477
+ GC = " \\| GC: (?<gc_duration>#{MSECS})ms"
478
+
479
+ COMPLETED_REGEXP = /I, \[#{TIMESTAMP} #[0-9]+\] INFO -- : \[#{LOG_ID}\] Completed #{STATUS} #{STATUS_IN_WORDS} in (?<total>#{MSECS})ms \(#{VIEWS}#{ACTIVE_RECORDS}(#{ALLOCATIONS}|#{GC})?\)/o
459
480
 
460
481
  def match_and_process_completed(line)
461
482
  matchdata = (COMPLETED_REGEXP.match line)
@@ -470,6 +491,9 @@ module LogSense
470
491
  duration_views_ms: matchdata[:views],
471
492
  duration_ar_ms: matchdata[:arec],
472
493
  allocations: matchdata[:alloc],
494
+ queries: matchdata[:queries],
495
+ cached_queries: matchdata[:cached_queries],
496
+ gc_duration: matchdata[:gc_duration],
473
497
  comment: ""
474
498
  }
475
499
  end
@@ -81,6 +81,22 @@ module LogSense
81
81
  }
82
82
  end
83
83
 
84
+ @queries = @db.execute %Q(
85
+ SELECT SUM(queries), SUM(cached_queries),
86
+ ROUND(SUM(queries) / SUM(cached_queries), 2)
87
+ FROM Event
88
+ )
89
+
90
+ @queries_by_controller = @db.execute %Q(
91
+ SELECT controller,
92
+ MIN(queries), MAX(queries), ROUND(AVG(queries), 2),
93
+ SUM(queries), SUM(cached_queries),
94
+ ROUND(SUM(cached_queries) / SUM(queries), 2),
95
+ SUM(gc_duration)
96
+ FROM Event
97
+ GROUP BY Event.controller
98
+ )
99
+
84
100
  @controller_and_methods_by_device = @db.execute %Q(
85
101
  SELECT controller as Controller,
86
102
  method as Method,
@@ -125,7 +141,7 @@ module LogSense
125
141
  ON event.log_id == error.log_id
126
142
  WHERE #{filter} and exit_status == 'S:FAILED'
127
143
  GROUP BY strftime("%Y-%m-%d", started_at)
128
- ).gsub("\n", "") || [[]]
144
+ ) || [[]]
129
145
 
130
146
  @fatal = @db.execute %(
131
147
  SELECT strftime("%Y-%m-%d %H:%M", started_at),
@@ -137,7 +153,7 @@ module LogSense
137
153
  FROM Event JOIN Error
138
154
  ON event.log_id == error.log_id
139
155
  WHERE #{filter} and exit_status == 'S:FAILED'
140
- ).gsub("\n", "") || [[]]
156
+ ) || [[]]
141
157
 
142
158
  @fatal_grouped = @db.execute %(
143
159
  SELECT filename,
@@ -147,7 +163,7 @@ module LogSense
147
163
  count(distinct(error.id))
148
164
  FROM Error
149
165
  GROUP BY description
150
- ).gsub("\n", "") || [[]]
166
+ ) || [[]]
151
167
 
152
168
  @job_plot = @db.execute %(
153
169
  SELECT strftime("%Y-%m-%d", ended_at) as Day,
@@ -156,7 +172,7 @@ module LogSense
156
172
  FROM Job
157
173
  WHERE #{filter}
158
174
  GROUP BY strftime("%Y-%m-%d", ended_at)
159
- ).gsub("\n", "") || [[]]
175
+ ) || [[]]
160
176
 
161
177
  # worker,
162
178
  # host,
@@ -173,7 +189,7 @@ module LogSense
173
189
  attempt
174
190
  FROM Job
175
191
  WHERE #{filter}
176
- ).gsub("\n", "") || [[]]
192
+ ) || [[]]
177
193
 
178
194
  @job_error_grouped = @db.execute %(
179
195
  SELECT worker,
@@ -188,7 +204,7 @@ module LogSense
188
204
  FROM Job
189
205
  WHERE #{filter} and (exit_status == 'S:ERROR' or exit_status == 'S:FAILED')
190
206
  GROUP BY object_id
191
- ).gsub("\n", "") || [[]]
207
+ ) || [[]]
192
208
 
193
209
  instance_vars_to_hash
194
210
  end
@@ -46,6 +46,8 @@ module LogSense
46
46
  total_statuses(data),
47
47
  daily_statuses(data),
48
48
  performance_over_time(data, col: "small-12 cell"),
49
+ queries(data),
50
+ queries_by_controller(data),
49
51
  {
50
52
  title: "Rails Performance",
51
53
  header: %w[Controller Hits Min Avg Max],
@@ -136,6 +136,29 @@ module LogSense
136
136
  }
137
137
  end
138
138
 
139
+ def queries(data, colors: [], col: "small-12 cell")
140
+ {
141
+ title: "Number of queries",
142
+ header: %w[Queries Cached Perc_Cached],
143
+ column_alignment: %i[right right right],
144
+ rows: data[:queries],
145
+ col: col,
146
+ }
147
+ end
148
+
149
+ def queries_by_controller(data, colors: [], col: "small-12 cell")
150
+ {
151
+ title: "Queries by Controller",
152
+ header: ["Controller",
153
+ "Min queries", "Max queries", "Avg Queries",
154
+ "Total queries", "Cached queries", "Perc",
155
+ "Total GC"],
156
+ column_alignment: %i[left right right right right right right right],
157
+ rows: data[:queries_by_controller],
158
+ col: col,
159
+ }
160
+ end
161
+
139
162
  #
140
163
  # Reports shared between rails and apache/nginx
141
164
  #
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LogSense
4
- VERSION = "2.3.1"
4
+ VERSION = "2.5.0"
5
5
  end
data/log_sense.gemspec CHANGED
@@ -6,8 +6,18 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Adolfo Villafiorita"]
7
7
  spec.email = ["adolfo@shair.tech"]
8
8
 
9
- spec.summary = %q{Generate analytics for Rails and Apache/Nginx log file.}
10
- spec.description = %q{Generate analytics in HTML, txt, and SQLite format for Rails and Apache/Nginx log files.}
9
+ spec.summary = %q{Analyze Rails and Apache/Nginx log file, generating statistics and reports.}
10
+ spec.description = %q{log_sense is a command-line command which analyzes Rails and Nginx logs and
11
+ provides detailed data about accesses, performances, and errors. It is a
12
+ one stop shop solution for analyzing your Rails application in production.
13
+
14
+ Simple to use and working offline, it does not require any modification to
15
+ your app (e.g., no need to change the log format, no cookies, no JavaScript
16
+ code, no infrastructure to setup).
17
+
18
+ We use it to monitor our applications in production, using a cron job which
19
+ pulls the logs and calls log_sense to generate HTML reports.
20
+ }
11
21
  spec.homepage = "https://github.com/shair-tech/log_sense/"
12
22
  spec.license = "MIT"
13
23
  spec.required_ruby_version = Gem::Requirement.new(">= 2.6.9")
@@ -26,7 +36,9 @@ Gem::Specification.new do |spec|
26
36
  spec.bindir = "exe"
27
37
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
38
  spec.require_paths = ["lib"]
29
-
39
+
40
+ spec.add_dependency "ostruct"
41
+ spec.add_dependency "csv"
30
42
  spec.add_dependency "browser", "~> 6.0.0"
31
43
  spec.add_dependency "ipaddr", "~> 1.2.0"
32
44
  spec.add_dependency "iso_country_codes", "~> 0.7.0"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: log_sense
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adolfo Villafiorita
@@ -9,6 +9,34 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ostruct
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: csv
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
12
40
  - !ruby/object:Gem::Dependency
13
41
  name: browser
14
42
  requirement: !ruby/object:Gem::Requirement
@@ -135,8 +163,14 @@ dependencies:
135
163
  - - ">="
136
164
  - !ruby/object:Gem::Version
137
165
  version: '0'
138
- description: Generate analytics in HTML, txt, and SQLite format for Rails and Apache/Nginx
139
- log files.
166
+ description: "log_sense is a command-line command which analyzes Rails and Nginx logs
167
+ and\n provides detailed data about accesses, performances, and errors. It is
168
+ a\n one stop shop solution for analyzing your Rails application in production.\n\n
169
+ \ Simple to use and working offline, it does not require any modification to\n
170
+ \ your app (e.g., no need to change the log format, no cookies, no JavaScript\n
171
+ \ code, no infrastructure to setup).\n\n We use it to monitor our applications
172
+ in production, using a cron job which\n pulls the logs and calls log_sense to
173
+ generate HTML reports.\n "
140
174
  email:
141
175
  - adolfo@shair.tech
142
176
  executables:
@@ -218,5 +252,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
252
  requirements: []
219
253
  rubygems_version: 3.6.7
220
254
  specification_version: 4
221
- summary: Generate analytics for Rails and Apache/Nginx log file.
255
+ summary: Analyze Rails and Apache/Nginx log file, generating statistics and reports.
222
256
  test_files: []