logster 0.8.3 → 0.8.4.pre

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: 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