logster 0.8.3 → 0.8.4.pre

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: fd434bdaf7b3450494fbb5bedbd054c787f5380f
4
- data.tar.gz: 80d5efd8cfe47417ef1f8f10166244ec302be26f
3
+ metadata.gz: 357588b2742ffeef436fe0367522604fccf12668
4
+ data.tar.gz: 694af6857f570a5ba4a7ceb670407a9a296f29f2
5
5
  SHA512:
6
- metadata.gz: e020fbe0569c7d982b7de1086eb54c0bf5be41e14a9de8bd045b7ce5ee4ead50c294754380fbf153d2deac1894d81e3c7d32cc207372c5846ea498af206460cb
7
- data.tar.gz: 403102d56cc8975d7df6a7a6e189084c89e49bc1447ea45d8b51dc0db407ea189c0114933d9bfa3ea8d4bf82760fa60cce4c0f2b1add3b4b962f062c57c315bb
6
+ metadata.gz: 14d5ff717da5d8b4c22759846ec5ec71e16a848844762f5417e1769b29de629dfe972c38631b8dad2e31ca357ada9dea387ce63e18c7983027a6dc83a6a9c6cf
7
+ data.tar.gz: 9453d1b7016dcf744e7d6d79e8ef0eb6b9a2b0df97d6f159ac46fcc2747359762e0a3bdfce2318d55b1b3a29b6b9e4dc6b30850777b0d92ac1faac8f39dda101
data/README.md CHANGED
@@ -25,6 +25,14 @@ constraints lambda { |req| req.session["admin"] } do
25
25
  end
26
26
  ```
27
27
 
28
+ By default, logster will only run in development and production environments.
29
+
30
+ To run logster in other environments, in `config/application.rb`
31
+
32
+ ```
33
+ Logster.set_environments([:development, :staging, :production])
34
+ ```
35
+
28
36
  ### Note
29
37
  If you are seeing error
30
38
  'No such middleware to insert before: ActionDispatch::DebugExceptions' after installing logster,
@@ -62,7 +70,7 @@ Logster UI is built using [Ember.js](http://emberjs.com/)
62
70
 
63
71
  ## Contributing
64
72
 
65
- 1. Fork it ( https://github.com/SamSaffron/logster/fork )
73
+ 1. Fork it ( https://github.com/discourse/logster/fork )
66
74
  2. Create your feature branch (`git checkout -b my-new-feature`)
67
75
  3. Commit your changes (`git commit -am 'Add some feature'`)
68
76
  4. Push to the branch (`git push origin my-new-feature`)
@@ -50,6 +50,49 @@ App.Router.map(function() {
50
50
  this.route("show", { path: "/show/:id" });
51
51
  });
52
52
 
53
+ function buildArrayString(array) {
54
+ var buffer = [];
55
+ _.each(array, function(v) {
56
+ if (v === null) {
57
+ buffer.push('null');
58
+ } else if (Object.prototype.toString.call(v) === '[object Array]') {
59
+ buffer.push(buildArrayString(v));
60
+ } else {
61
+ buffer.push(v.toString());
62
+ }
63
+ });
64
+ return '[' + buffer.join(', ') + ']';
65
+ }
66
+
67
+ function buildHashString(hash, indent) {
68
+ if (!hash) return '';
69
+ if (!indent) indent = "";
70
+
71
+ var buffer = [],
72
+ hashes = [];
73
+ _.each(hash, function(v, k) {
74
+ if (v === null) {
75
+ buffer.push('null');
76
+ } else if (Object.prototype.toString.call(v) === '[object Array]') {
77
+ buffer.push(k + ": " + buildArrayString(v));
78
+ } else if (typeof v === "object") {
79
+ hashes.push(k);
80
+ } else {
81
+ buffer.push(k + ": " + v);
82
+ }
83
+ });
84
+
85
+ if (_.size(hashes) > 0) {
86
+ _.each(hashes, function(k1) {
87
+ var v = hash[k1];
88
+ buffer.push("");
89
+ buffer.push(k1 + ":");
90
+ buffer.push(buildHashString(v, indent + " "));
91
+ });
92
+ }
93
+ return indent + buffer.join("\n" + indent);
94
+ }
95
+
53
96
  App.Message = Ember.Object.extend({
54
97
 
55
98
  MAX_LEN: 200,
@@ -67,6 +110,10 @@ App.Message = Ember.Object.extend({
67
110
  return App.ajax("/unprotect/" + this.get('key'), { type: "DELETE" });
68
111
  },
69
112
 
113
+ showCount: function() {
114
+ return this.get('count') > 1;
115
+ }.property('count'),
116
+
70
117
  hasMore: function() {
71
118
  var message = this.get("message");
72
119
  var expanded = this.get("expanded");
@@ -92,32 +139,13 @@ App.Message = Ember.Object.extend({
92
139
  return message;
93
140
  }.property("message", "expanded"),
94
141
 
95
- envDebug: function() {
96
- var env = this.get("env");
97
- if (env) {
98
- var buffer = [],
99
- hashes = [];
100
- _.each(env, function(v, k) {
101
- if (typeof v === "object") {
102
- hashes.push(k);
103
- } else {
104
- buffer.push(k + ": " + v);
105
- }
106
- });
107
-
108
- if (_.size(hashes) > 0) {
109
- _.each(hashes, function(k1) {
110
- v1 = env[k1];
111
- buffer.push("");
112
- buffer.push(k1 + ":");
113
- _.each(v1, function(v2, k2) {
114
- buffer.push(" " + k2 + ": " + v2);
115
- })
116
- });
117
- }
118
- return buffer.join("\n");
119
- }
142
+ updateFromObject: function(other) {
143
+ // XXX Only updatable property is count right now
144
+ this.set('count', other.get('count'));
145
+ },
120
146
 
147
+ envDebug: function() {
148
+ return buildHashString(this.get('env'));
121
149
  }.property("env"),
122
150
 
123
151
  rowClass: function() {
@@ -154,6 +182,7 @@ App.Message = Ember.Object.extend({
154
182
  App.MessageCollection = Em.Object.extend({
155
183
 
156
184
  messages: Em.A(),
185
+ currentMessage: null,
157
186
  total: 0,
158
187
 
159
188
  load: function(opts) {
@@ -190,6 +219,18 @@ App.MessageCollection = Em.Object.extend({
190
219
  if (opts.before) {
191
220
  messages.unshiftObjects(newRows);
192
221
  } else {
222
+ newRows.forEach(function(nmsg) {
223
+ messages.forEach(function(emsg, idx) {
224
+ if (emsg.key == nmsg.key) {
225
+ messages.removeObject(emsg);
226
+ if (self.get('currentMessage') === emsg) {
227
+ // TODO would updateFromJson() work here?
228
+ self.set('currentMessage', nmsg);
229
+ nmsg.set('selected', emsg.get('selected'));
230
+ }
231
+ }
232
+ });
233
+ });
193
234
  messages.addObjects(newRows);
194
235
  }
195
236
  }
@@ -293,11 +334,18 @@ App.ShowRoute = Em.Route.extend({
293
334
  });
294
335
 
295
336
  App.IndexController = Em.Controller.extend({
337
+
338
+ currentMessage: Em.computed.alias('model.currentMessage'),
339
+
296
340
  actions: {
297
341
  expandMessage: function(message){
298
342
  message.expand();
299
343
  },
300
344
 
345
+ selectMessage: function(message) {
346
+ this.set('currentMessage', message);
347
+ },
348
+
301
349
  showMoreBefore: function(){
302
350
  this.get('model').showMoreBefore();
303
351
  },
@@ -468,9 +516,9 @@ App.MessageView = Em.View.extend({
468
516
 
469
517
  classNameBindings: ["context.rowClass", ":message-row", "context.selected:selected"],
470
518
 
471
- click: function(){
519
+ click: function() {
472
520
  var old = this.get("controller.currentMessage");
473
- if(old){
521
+ if (old) {
474
522
  old.set("selected",false);
475
523
  }
476
524
  this.set("context.selected", true);
@@ -2,7 +2,11 @@
2
2
  {{#tabbed-section}}
3
3
  {{#tab-contents name="info" hint="show info" currentMessage=currentMessage}}
4
4
  {{#if showTitle}}
5
- <h3>Message</h3>
5
+ <h3>Message
6
+ {{#if currentMessage.showCount}}
7
+ ({{currentMessage.count}} copies reported)
8
+ {{/if}}
9
+ </h3>
6
10
  {{/if}}
7
11
  <pre>{{currentMessage.message}}</pre>
8
12
  {{/tab-contents}}
@@ -1,4 +1,8 @@
1
- <td class="severity">{{{glyph}}}</td>
1
+ <td class="severity">{{{glyph}}}
2
+ {{#if showCount}}
3
+ <span class='count'>{{count}}</span>
4
+ {{/if}}
5
+ </td>
2
6
  <td class="message-body">
3
7
  <div class="message">
4
8
  {{displayMessage}}
@@ -36,7 +36,7 @@ td.time {
36
36
  vertical-align: top;
37
37
  }
38
38
  .message {
39
- font-family: Consolas, mono;
39
+ font-family: Consolas, Monaco, Ubuntu Mono, monospace;
40
40
  font-size: 13px;
41
41
  overflow: hidden;
42
42
  white-space: nowrap;
@@ -45,7 +45,7 @@ td.time {
45
45
  }
46
46
 
47
47
  th.severity{
48
- width: 30px;
48
+ width: 35px;
49
49
  }
50
50
 
51
51
  th.time{
@@ -55,18 +55,38 @@ th.time{
55
55
  .fatal{
56
56
  color: #E00;
57
57
  }
58
+ .fatal .count {
59
+ background-color: #E00;
60
+ }
58
61
 
59
62
  .error {
60
63
  color: #9F6000;
61
64
  }
65
+ .error .count {
66
+ background-color: #9F6000;
67
+ }
62
68
 
63
- .warn{
69
+ .warn {
64
70
  color: #66000C;
65
71
  }
72
+ .warn .count {
73
+ background-color: #66000C;
74
+ }
66
75
 
67
- .debug{
76
+ .debug {
68
77
  color: #777;
69
78
  }
79
+ .debug .count {
80
+ background-color: #666;
81
+ }
82
+
83
+ .count {
84
+ color: #FFF;
85
+ border-radius: 3px;
86
+ padding: 0 4px;
87
+ float: right;
88
+ margin-right: 6px;
89
+ }
70
90
 
71
91
  .action-panel .search input {
72
92
  border: 1px solid #DDD;
data/lib/logster.rb CHANGED
@@ -40,9 +40,14 @@ module Logster
40
40
  logster_env = Logster::Message.populate_from_env(env)
41
41
  logster_env[key] = value
42
42
  end
43
+
44
+ def self.set_environments(envs)
45
+ @config.environments = envs
46
+ end
43
47
  end
44
48
 
45
- Logster.config.current_context = lambda{ |env, &block| block.call }
49
+ # check logster/configuration.rb for config options
50
+ # Logster.config.environments << :staging
46
51
 
47
52
  if defined?(::Rails) && ::Rails::VERSION::MAJOR.to_i >= 3
48
53
  require 'logster/rails/railtie'
@@ -10,30 +10,48 @@ module Logster
10
10
  @skip_empty = true
11
11
  end
12
12
 
13
+ # Save a new message at the front of the latest list.
13
14
  def save(message)
14
15
  not_implemented
15
16
  end
16
17
 
18
+ # Modify the saved message to the given one (identified by message.key) and bump it to the top of the latest list
19
+ def replace_and_bump(message)
20
+ not_implemented
21
+ end
22
+
23
+ # Check if another message with the same grouping_key is already stored.
24
+ # Returns the similar message's message.key
25
+ def similar_key(message)
26
+ not_implemented
27
+ end
28
+
29
+ # The number of messages currently stored.
17
30
  def count
18
31
  not_implemented
19
32
  end
20
33
 
34
+ # Delete all unprotected messages in the store.
21
35
  def clear
22
36
  not_implemented
23
37
  end
24
38
 
39
+ # Delete all messages, including protected messages.
25
40
  def clear_all
26
41
  not_implemented
27
42
  end
28
43
 
44
+ # Get a message by its message_key
29
45
  def get(message_key)
30
46
  not_implemented
31
47
  end
32
48
 
49
+ # Mark a message as protected; i.e. it is not deleted by the #clear method
33
50
  def protect(message_key)
34
51
  not_implemented
35
52
  end
36
53
 
54
+ # Clear the protected mark for a message.
37
55
  def unprotect(message_key)
38
56
  not_implemented
39
57
  end
@@ -65,9 +83,23 @@ module Logster
65
83
 
66
84
  return if ignore && ignore.any? { |pattern| message =~ pattern}
67
85
 
68
- save message
86
+ similar = nil
69
87
 
70
- message
88
+ if Logster.config.allow_grouping
89
+ key = self.similar_key(message)
90
+ similar = get key if key
91
+ end
92
+
93
+ if similar
94
+ similar.count += 1
95
+ similar.merge_similar_message(message)
96
+
97
+ replace_and_bump similar
98
+ similar
99
+ else
100
+ save message
101
+ message
102
+ end
71
103
  end
72
104
 
73
105
  private
@@ -1,5 +1,22 @@
1
1
  module Logster
2
2
  class Configuration
3
- attr_accessor :subdirectory, :current_context
3
+ attr_accessor :current_context, :allow_grouping, :environments
4
+ attr_writer :subdirectory
5
+
6
+ def initialize
7
+ # lambda |env,block|
8
+ @current_context = lambda{ |_, &block| block.call }
9
+ @environments = [:development, :production]
10
+ @subdirectory = nil
11
+
12
+ @allow_grouping = false
13
+ if defined?(::Rails) && Rails.env.production?
14
+ @allow_grouping = true
15
+ end
16
+ end
17
+
18
+ def subdirectory
19
+ @subdirectory || '/logs'
20
+ end
4
21
  end
5
22
  end
@@ -1,4 +1,9 @@
1
+ require 'digest/sha1'
2
+
1
3
  module Logster
4
+
5
+ MAX_GROUPING_LENGTH = 50
6
+
2
7
  class Message
3
8
  LOGSTER_ENV = "_logster_env".freeze
4
9
  ALLOWED_ENV = %w{
@@ -69,6 +74,26 @@ module Logster
69
74
  @env = Message.populate_from_env(env)
70
75
  end
71
76
 
77
+ # in its own method so it can be overridden
78
+ def grouping_hash
79
+ return { message: self.message, severity: self.severity, backtrace: self.backtrace }
80
+ end
81
+
82
+ # todo - memoize?
83
+ def grouping_key
84
+ Digest::SHA1.hexdigest JSON.fast_generate grouping_hash
85
+ end
86
+
87
+ def is_similar?(other)
88
+ self.grouping_key == other.grouping_key
89
+ end
90
+
91
+ def merge_similar_message(other)
92
+ other_env = JSON.load JSON.fast_generate other.env
93
+ other_env.keys.each do |env_key|
94
+ self.env[env_key] = Message.env_merge_helper(self.env[env_key], other_env[env_key])
95
+ end
96
+ end
72
97
 
73
98
  def self.populate_from_env(env)
74
99
  env[LOGSTER_ENV] ||= begin
@@ -82,6 +107,8 @@ module Logster
82
107
  request.params.each do |k,v|
83
108
  if k.include? "password"
84
109
  params[k] = "[redacted]"
110
+ elsif Array === v
111
+ params[k] = v[0..20]
85
112
  else
86
113
  params[k] = v && v[0..100]
87
114
  end
@@ -121,5 +148,48 @@ module Logster
121
148
  def get_timestamp
122
149
  (Time.new.to_f * 1000).to_i
123
150
  end
151
+
152
+ private
153
+
154
+ def self.env_merge_helper(self_value, other_value)
155
+ other_value = other_value.to_s if Symbol === other_value
156
+
157
+ if (Hash === self_value || self_value.nil?) && (Hash === other_value || other_value.nil?) && (!self_value.nil? || !other_value.nil?)
158
+ # one or both is a hash but not neither -> recurse on the keys
159
+ self_value = {} unless self_value
160
+ other_value = {} unless other_value
161
+ shared_keys = self_value.keys | (other_value.keys rescue [])
162
+ shared_keys.each do |key|
163
+ self_value[key] = env_merge_helper(self_value[key], other_value[key])
164
+ end
165
+ self_value
166
+ elsif self_value.is_a?(Array) && !other_value.is_a?(Array)
167
+ # Already have grouped data, so append to array (it's actually a set)
168
+ self_value << other_value unless self_value.include?(other_value) || self_value.length >= Logster::MAX_GROUPING_LENGTH
169
+ self_value
170
+ elsif !self_value.is_a?(Array)
171
+ if self_value == other_value
172
+ self_value
173
+ else
174
+ [self_value, other_value]
175
+ end
176
+ else
177
+ # They're both arrays.
178
+ # Three cases:
179
+ # self = [1,2,3] and other = [1,2,4] -> make into array of array
180
+ # self = [] and other = [1,2,4] -> make into array of array
181
+ # self = [[1,2,3], [1,2,5]] and other = [1,2,4] -> append to array
182
+ if self_value.length > 0 && self_value[0].is_a?(Array)
183
+ self_value << other_value unless self_value.include?(other_value) || self_value.length >= Logster::MAX_GROUPING_LENGTH
184
+ self_value
185
+ else
186
+ if self_value == other_value
187
+ self_value
188
+ else
189
+ [self_value, other_value]
190
+ end
191
+ end
192
+ end
193
+ end
124
194
  end
125
195
  end
@@ -7,7 +7,7 @@ module Logster
7
7
 
8
8
  def initialize(app, config={})
9
9
  @app = app
10
- @error_path = (Logster.config.subdirectory || '/logs') + '/report_js_error'
10
+ @error_path = Logster.config.subdirectory + '/report_js_error'
11
11
  end
12
12
 
13
13
  def call(env)
@@ -11,7 +11,7 @@ module Logster
11
11
  def initialize(app)
12
12
  @app = app
13
13
 
14
- @logs_path = Logster.config.subdirectory || "/logs"
14
+ @logs_path = Logster.config.subdirectory
15
15
  @path_regex = Regexp.new("^(#{@logs_path}$)|^(#{@logs_path}(/.*))$")
16
16
  @store = Logster.store or raise ArgumentError.new("store")
17
17
 
@@ -5,7 +5,7 @@ module Logster::Rails
5
5
  end
6
6
 
7
7
  def self.set_logger(config)
8
- return unless Rails.env.development? || Rails.env.production?
8
+ return unless Logster.config.environments.include?(Rails.env.to_sym)
9
9
 
10
10
  require 'logster/middleware/debug_exceptions'
11
11
  require 'logster/middleware/reporter'
@@ -22,7 +22,7 @@ module Logster::Rails
22
22
 
23
23
 
24
24
  def self.initialize!(app)
25
- return unless Rails.env.development? || Rails.env.production?
25
+ return unless Logster.config.environments.include?(Rails.env.to_sym)
26
26
 
27
27
  if Logster::Logger === Rails.logger
28
28
  app.middleware.insert_before ActionDispatch::ShowExceptions, Logster::Middleware::Reporter
@@ -13,21 +13,41 @@ module Logster
13
13
  end
14
14
 
15
15
  def save(message)
16
- # multi for integrity
16
+ # TODO this whole method should be atomic lol, but it hasn't bitten me yet
17
17
  @redis.multi do
18
18
  @redis.hset(hash_key, message.key, message.to_json)
19
+ @redis.hset(grouping_key, message.grouping_key, message.key)
19
20
  @redis.rpush(list_key, message.key)
20
21
  end
21
22
 
22
- # TODO make it atomic
23
23
  if @redis.llen(list_key) > max_backlog
24
24
  removed_key = @redis.lpop(list_key)
25
25
  if removed_key && !@redis.sismember(protected_key, removed_key)
26
- @redis.hdel(hash_key, removed_key)
26
+ rmsg = get removed_key
27
+ @redis.hdel(hash_key, rmsg.key)
28
+ @redis.hdel(grouping_key, rmsg.grouping_key)
27
29
  end
28
30
  end
29
31
  end
30
32
 
33
+ def replace_and_bump(message)
34
+ # TODO make it atomic
35
+ exists = @redis.hexists(hash_key, message.key)
36
+ return false unless exists
37
+
38
+ @redis.multi do
39
+ @redis.hset(hash_key, message.key, message.to_json)
40
+ @redis.lrem(list_key, -1, message.key)
41
+ @redis.rpush(list_key, message.key)
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ def similar_key(message)
48
+ @redis.hget(grouping_key, message.grouping_key)
49
+ end
50
+
31
51
  def count
32
52
  @redis.llen(list_key)
33
53
  end
@@ -108,6 +128,7 @@ module Logster
108
128
  @redis.del(list_key)
109
129
  @redis.del(protected_key)
110
130
  @redis.del(hash_key)
131
+ @redis.del(grouping_key)
111
132
  end
112
133
 
113
134
  def get(message_key)
@@ -233,5 +254,9 @@ module Logster
233
254
  def protected_key
234
255
  @saved_key ||= "__LOGSTER__SAVED"
235
256
  end
257
+
258
+ def grouping_key
259
+ @grouping_key ||= "__LOGSTER__GMAP"
260
+ end
236
261
  end
237
262
  end
@@ -1,3 +1,3 @@
1
1
  module Logster
2
- VERSION = "0.8.3"
2
+ VERSION = "0.8.4.pre"
3
3
  end
data/logster.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["sam.saffron@gmail.com"]
11
11
  spec.summary = %q{UI for viewing logs in Rack}
12
12
  spec.description = %q{UI for viewing logs in Rack}
13
- spec.homepage = ""
13
+ spec.homepage = "https://github.com/discourse/logster"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files -z`.split("\x0").reject{|f| f.start_with?("bower_components") || f.start_with?("website") || f.start_with?("bin") }
@@ -30,7 +30,7 @@
30
30
  err.stacktrace = errorObj.stack;
31
31
  }
32
32
 
33
- $.ajax("<%= Logster.config.subdirectory || '/logs' %>" + "/report_js_error", {
33
+ $.ajax("<%= Logster.config.subdirectory %>" + "/report_js_error", {
34
34
  data: err,
35
35
  type: "POST",
36
36
  cache: false
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logster
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.8.4.pre
5
5
  platform: ruby
6
6
  authors:
7
7
  - UI for viewing logs in Rack
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-16 00:00:00.000000000 Z
11
+ date: 2015-08-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -148,7 +148,7 @@ files:
148
148
  - test/logster/test_redis_store.rb
149
149
  - test/test_helper.rb
150
150
  - vendor/assets/javascripts/logster.js.erb
151
- homepage: ''
151
+ homepage: https://github.com/discourse/logster
152
152
  licenses:
153
153
  - MIT
154
154
  metadata: {}
@@ -163,9 +163,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
163
  version: '0'
164
164
  required_rubygems_version: !ruby/object:Gem::Requirement
165
165
  requirements:
166
- - - ">="
166
+ - - ">"
167
167
  - !ruby/object:Gem::Version
168
- version: '0'
168
+ version: 1.3.1
169
169
  requirements: []
170
170
  rubyforge_project:
171
171
  rubygems_version: 2.4.5