log_changes 0.1.0 → 1.0.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
  SHA1:
3
- metadata.gz: cac50df66f4bbf5b431b1594c2ae8b051ce81fc4
4
- data.tar.gz: 836919fc6c68e35d9abaaec218d9b7f81a46755b
3
+ metadata.gz: b1470104760459fde7eb28abce175dc5c94e2157
4
+ data.tar.gz: f1e02001e4ea961d395d8b3301767b807764dd4b
5
5
  SHA512:
6
- metadata.gz: 8490c5addfef1fa4d696598c4c9b56b333c936cdcf659853192870c25a011a1477a8cb4bded905bdb6425961877580ef8c21dccd433429529d1c8b3bb7623596
7
- data.tar.gz: d20b3e85a7aa3de3e88389e7a02943c4cf20cd7e10f045d1f3549b86580b750afcd1bdcb31425dcc32baaf3c630a3bf821699f8a3861c51ad68eb97f5baf86bc
6
+ metadata.gz: 7bb15684947c874a4f972bc53b09fa81c91c2249fd3994020aad8eff94bcdaf26a69d180a1628048ae039bb3d28907dff529fbb3f23d2648159a279d259fd072
7
+ data.tar.gz: f47441a7a0d1e087ed50242c2b013fce2905d9f38cbf4a930409b2fbbcfe486f87729eefdc35dd352682ffe7dc162b85f165afce3ef952da7bbd4f0ae86d5f0d
data/README.md CHANGED
@@ -1,8 +1,44 @@
1
1
  # LogChanges
2
- Short description and motivation.
2
+
3
+ `log_changes` is a simple gem that writes `ActiveRecord#changes` contents to model-dedicated logfiles.
3
4
 
4
5
  ## Usage
5
- How to use my plugin.
6
+ To log attribute changes for a model simply add `include LogChanges::Base` to an `ActiveRecord` model:
7
+
8
+ ```ruby
9
+ class User < ApplicationRecord
10
+ include LogChanges::Base
11
+
12
+ def to_s
13
+ "#{first_name} #{last_name}"
14
+ end
15
+ end
16
+ ```
17
+
18
+ In this example, if the `User` record with id = 1 had his name updated you may see an entry like this appear `logs/record_changes/2016.12_User.log`:
19
+
20
+ ```
21
+ 12/29/2016 at 3:48 PM (UTC)
22
+ Updated User {id: 1} John Smith
23
+ first_name:
24
+ FROM: Johnny
25
+ TO: John
26
+ last_name:
27
+ FROM: Smithers
28
+ TO: Smith
29
+ ```
30
+
31
+ Logfiles are prefixed with a month stamp (to prevent them from getting too big over time).
32
+
33
+ ### Aggregating logfiles
34
+
35
+ If your Rails app runs in a load-balanced distributed environment, you may wish to aggregate logs from multiple servers. `log_changes` comes with a rake task for this purpose:
36
+
37
+ ```
38
+ rake log_changes:merge['/path/to/logs/directory']
39
+ ```
40
+
41
+ Make sure the path you pass to the task has multiple subfolders (one per server), each with a `record_changes` directory containing the log files.
6
42
 
7
43
  ## Installation
8
44
  Add this line to your application's Gemfile:
@@ -13,16 +49,8 @@ gem 'log_changes'
13
49
 
14
50
  And then execute:
15
51
  ```bash
16
- $ bundle
52
+ $ bundle install
17
53
  ```
18
54
 
19
- Or install it yourself as:
20
- ```bash
21
- $ gem install log_changes
22
- ```
23
-
24
- ## Contributing
25
- Contribution directions go here.
26
-
27
55
  ## License
28
56
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/lib/log_changes.rb CHANGED
@@ -2,5 +2,6 @@ require 'log_changes/engine'
2
2
  require 'sr_log'
3
3
 
4
4
  module LogChanges
5
- autoload :Base, 'log_changes/base'
5
+ autoload :Base, 'log_changes/base'
6
+ autoload :Merge, 'log_changes/merge'
6
7
  end
@@ -0,0 +1,123 @@
1
+ module LogChanges
2
+ module Merge
3
+ def merge_logfiles( log_dir )
4
+ [true, false].each do |record_changes|
5
+ logfiles = associate_logfiles( log_dir, record_changes )
6
+
7
+ aggregate_log_dir = File.expand_path( File.join(log_dir, "../#{Time.now.in_time_zone.strftime('%Y-%m-%d_%H%M')}_aggregated_logs#{record_changes ? '/record_changes' : ''}") )
8
+ FileUtils.mkdir_p( aggregate_log_dir )
9
+
10
+ logfiles.each do |log_type, logfile_paths|
11
+ entries = []
12
+ logfile_paths.each do |lf|
13
+ lf_entries = logfile_entries(lf)
14
+ if lf_entries.nil?
15
+ puts "\nSKIPPING logfile (unable to parse entries): #{lf}\n\n"
16
+ else
17
+ entries += lf_entries
18
+ end
19
+ end
20
+ next if entries.empty?
21
+ entries.sort!{|e1, e2| e1[:time] <=> e2[:time]}
22
+ merged_log = File.join(aggregate_log_dir, "#{log_type}_#{entries.first[:time].in_time_zone.strftime('%Y-%m-%d_%H%M')}_-_#{entries.last[:time].in_time_zone.strftime('%Y-%m-%d_%H%M')}.log")
23
+ File.open( merged_log, 'w' ) do |file|
24
+ entries.each do |entry|
25
+ file.write "#{entry[:time].strftime('%-m/%-d/%Y at %-l:%M %p (%Z)')}\n"
26
+ file.write "#{entry[:text]}\n\n"
27
+ end
28
+ end
29
+ puts "Merged log: #{merged_log}\n #{entries.length} #{'entry'.pluralize(entries.length)}"
30
+ end
31
+ end
32
+ end
33
+
34
+ # Returns a hash whose keys are the common log names and whose values are arrays of file paths, e.g.:
35
+ # {
36
+ # "ajax_errors" => [
37
+ # [ 0] "/Users/seanhuber/Downloads/record_changes/2015.08_ajax_errors.log",
38
+ # [ 1] "/Users/seanhuber/Downloads/record_changes/2015.09_ajax_errors.log",
39
+ # [ 2] "/Users/seanhuber/Downloads/record_changes/2015.10_ajax_errors.log"
40
+ # ],
41
+ # "care_plan_updates" => [
42
+ # [ 0] "/Users/seanhuber/Downloads/record_changes/2015.08_care_plan_updates.log",
43
+ # [ 1] "/Users/seanhuber/Downloads/record_changes/2015.09_care_plan_updates.log",
44
+ # [ 2] "/Users/seanhuber/Downloads/record_changes/2015.10_care_plan_updates.log"
45
+ # ],
46
+ # "eval_updates" => [
47
+ # [ 0] "/Users/seanhuber/Downloads/record_changes/2015.08_eval_updates.log",
48
+ # [ 1] "/Users/seanhuber/Downloads/record_changes/2015.09_eval_updates.log",
49
+ # [ 2] "/Users/seanhuber/Downloads/record_changes/2015.10_eval_updates.log"
50
+ # ],
51
+ # }
52
+ def associate_logfiles( log_dir, record_changes = false )
53
+ raise "Directory does not exist: #{log_dir}" unless File.directory?( log_dir )
54
+
55
+ # scans for logfiles prefixed with a month stamp like "2016.03_"
56
+ ret_h = {}
57
+ search_path = record_changes ? File.join(log_dir, '**', 'record_changes', '*.log') : File.join(log_dir, '**', '20*_*.log')
58
+ Dir.glob(search_path) do |log_fp|
59
+ next if !record_changes && (log_fp.include?('record_changes') || log_fp.include?('import')) # TODO: include import
60
+ month_stamp = File.basename(log_fp).split('_').first
61
+ next unless month_stamp.length == 7 && month_stamp[4] == '.'
62
+ begin
63
+ DateTime.strptime(month_stamp, '%Y.%m')
64
+ rescue ArgumentError
65
+ next
66
+ end
67
+ log_class = File.basename(log_fp, File.extname(log_fp)).split('_')[1..-1].join('_')
68
+ ret_h[log_class] ||= []
69
+ ret_h[log_class] << log_fp
70
+ end
71
+ ret_h
72
+ end
73
+
74
+ # Returns an array of hashes containing time and text of each log entry, e.g.,
75
+ # [
76
+ # [0] {
77
+ # :time => Thu, 17 Sep 2015 12:20:00 -0500,
78
+ # :text => "Logged by user: (bm25671) John Doe\nSome message was logged"
79
+ # },
80
+ # [1] {
81
+ # :time => Thu, 17 Sep 2015 12:27:00 -0500,
82
+ # :text => "Logged by user: (bm25671) Jane Smith\nLorem ipsum..."
83
+ # },
84
+ # [2] {
85
+ # :time => Thu, 17 Sep 2015 13:24:00 -0500,
86
+ # :text => "Logged by user: (vr16208) Charlie Williams\nblah blah blah"
87
+ # }
88
+ # ]
89
+ #
90
+ # Returns nil if entries couldn't be parsed (unable to find lines structured DateTime)
91
+ def logfile_entries( logfile )
92
+ lines = File.open( logfile ).map{|l| l}
93
+ entry_indexes = [] # lines that are just a timestamp
94
+ lines.each_with_index do |line, idx|
95
+ next unless first_char_is_num?( line )
96
+ dt = begin
97
+ DateTime.strptime(line.strip, "%m/%d/%Y at %l:%M %p (%Z)")
98
+ rescue ArgumentError
99
+ nil
100
+ end
101
+ next if dt.nil?
102
+ next if idx > 0 && lines[idx-1].strip.present? && !lines[idx-1].strip.starts_with?('Logged by user:')
103
+ entry_indexes << idx
104
+ end
105
+ return nil if entry_indexes.empty?
106
+
107
+ # TODO: refactor (shouldn't need to loop over the logfile twice)
108
+ entries = []
109
+ entry_indexes.each_with_index do |entry_idx, entry_indexes_idx|
110
+ end_idx = entry_indexes_idx == (entry_indexes.length - 1) ? (lines.length-1) : (entry_indexes[entry_indexes_idx+1]-1)
111
+ end_idx -= 1 if lines[end_idx].starts_with?('Logged by user:')
112
+ entry_text = lines[entry_idx+1..end_idx].join
113
+ entry_text = lines[entry_idx-1] + entry_text if entry_idx > 0 && lines[entry_idx-1].starts_with?('Logged by user:')
114
+ entries << {:time => DateTime.strptime(lines[entry_idx].strip, "%m/%d/%Y at %l:%M %p (%Z)"), :text => entry_text.strip}
115
+ end
116
+ entries
117
+ end
118
+
119
+ def first_char_is_num?( str )
120
+ !(str[0] =~ /^\d/).nil?
121
+ end
122
+ end
123
+ end
@@ -1,3 +1,3 @@
1
1
  module LogChanges
2
- VERSION = '0.1.0'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -0,0 +1,9 @@
1
+ namespace :log_changes do
2
+ desc 'aggregates logfiles created with log_changes'
3
+ task :merge, [:logs_path] do |_, args|
4
+ raise ArgumentError, 'No logs directory specified' unless args[:logs_path].present? && File.directory?(args[:logs_path])
5
+ include LogChanges::Merge
6
+ Time.zone = 'Central Time (US & Canada)' # for timestamping log files (TODO: is this needed?)
7
+ merge_logfiles args[:logs_path]
8
+ end
9
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: log_changes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Huber
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-12-28 00:00:00.000000000 Z
11
+ date: 2016-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -136,8 +136,10 @@ files:
136
136
  - lib/log_changes.rb
137
137
  - lib/log_changes/base.rb
138
138
  - lib/log_changes/engine.rb
139
+ - lib/log_changes/merge.rb
139
140
  - lib/log_changes/version.rb
140
141
  - lib/tasks/log_changes_tasks.rake
142
+ - lib/tasks/merge_logfiles.rake
141
143
  - log_changes.gemspec
142
144
  - spec/dummy/Rakefile
143
145
  - spec/dummy/app/assets/config/manifest.js