log_changes 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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