logster 2.5.1 → 2.6.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -0
  3. data/CHANGELOG.md +9 -0
  4. data/README.md +15 -1
  5. data/Rakefile +1 -0
  6. data/assets/javascript/client-app.js +204 -168
  7. data/assets/javascript/vendor.js +5132 -5833
  8. data/assets/stylesheets/client-app.css +1 -1
  9. data/client-app/.eslintrc.js +17 -5
  10. data/client-app/.travis.yml +4 -3
  11. data/client-app/app/app.js +5 -7
  12. data/client-app/app/components/actions-menu.js +24 -17
  13. data/client-app/app/components/back-trace.js +148 -0
  14. data/client-app/app/components/env-tab.js +16 -12
  15. data/client-app/app/components/message-info.js +84 -7
  16. data/client-app/app/components/message-row.js +13 -15
  17. data/client-app/app/components/panel-resizer.js +63 -45
  18. data/client-app/app/components/patterns-list.js +6 -6
  19. data/client-app/app/components/update-time.js +13 -13
  20. data/client-app/app/controllers/index.js +4 -2
  21. data/client-app/app/index.html +1 -1
  22. data/client-app/app/initializers/app-init.js +1 -1
  23. data/client-app/app/lib/decorators.js +11 -0
  24. data/client-app/app/lib/preload.js +14 -3
  25. data/client-app/app/lib/utilities.js +63 -36
  26. data/client-app/app/models/group.js +6 -1
  27. data/client-app/app/models/message-collection.js +9 -7
  28. data/client-app/app/models/message.js +25 -20
  29. data/client-app/app/router.js +4 -6
  30. data/client-app/app/styles/app.css +18 -4
  31. data/client-app/app/templates/components/actions-menu.hbs +6 -2
  32. data/client-app/app/templates/components/back-trace.hbs +8 -0
  33. data/client-app/app/templates/components/message-info.hbs +7 -2
  34. data/client-app/app/templates/index.hbs +4 -1
  35. data/client-app/config/environment.js +1 -1
  36. data/client-app/config/optional-features.json +4 -1
  37. data/client-app/ember-cli-build.js +2 -3
  38. data/client-app/package-lock.json +9712 -2884
  39. data/client-app/package.json +25 -22
  40. data/client-app/preload-json-manager.rb +62 -0
  41. data/client-app/testem.js +0 -1
  42. data/client-app/tests/index.html +1 -1
  43. data/client-app/tests/integration/components/back-trace-test.js +109 -0
  44. data/client-app/tests/integration/components/message-info-test.js +4 -3
  45. data/client-app/tests/integration/components/patterns-list-test.js +7 -2
  46. data/lib/logster.rb +1 -0
  47. data/lib/logster/base_store.rb +16 -9
  48. data/lib/logster/configuration.rb +12 -2
  49. data/lib/logster/defer_logger.rb +1 -1
  50. data/lib/logster/logger.rb +12 -0
  51. data/lib/logster/message.rb +89 -30
  52. data/lib/logster/middleware/viewer.rb +44 -8
  53. data/lib/logster/redis_store.rb +69 -51
  54. data/lib/logster/suppression_pattern.rb +1 -1
  55. data/lib/logster/version.rb +1 -1
  56. data/logster.gemspec +1 -1
  57. data/test/logster/middleware/test_viewer.rb +100 -0
  58. data/test/logster/test_base_store.rb +16 -0
  59. data/test/logster/test_defer_logger.rb +1 -1
  60. data/test/logster/test_message.rb +142 -54
  61. data/test/logster/test_redis_store.rb +99 -39
  62. metadata +11 -6
@@ -18,40 +18,43 @@
18
18
  "test": "ember test"
19
19
  },
20
20
  "devDependencies": {
21
- "@ember/optional-features": "^0.6.3",
22
- "broccoli-asset-rev": "^2.7.0",
23
- "ember-ajax": "^5.0.0",
24
- "ember-cli": "^3.8.3",
21
+ "@ember/optional-features": "^1.1.0",
22
+ "@glimmer/component": "^1.0.0",
23
+ "babel-eslint": "^10.0.3",
24
+ "broccoli-asset-rev": "^3.0.0",
25
+ "ember-auto-import": "^1.5.3",
26
+ "ember-cli": "^3.15.0",
25
27
  "ember-cli-app-version": "^3.2.0",
26
- "ember-cli-babel": "^7.12.0",
28
+ "ember-cli-babel": "^7.13.0",
27
29
  "ember-cli-dependency-checker": "^3.2.0",
28
- "ember-cli-eslint": "^4.2.3",
29
- "ember-cli-htmlbars": "^3.1.0",
30
- "ember-cli-htmlbars-inline-precompile": "^1.0.3",
31
- "ember-cli-inject-live-reload": "^1.8.2",
30
+ "ember-cli-eslint": "^5.1.0",
31
+ "ember-cli-htmlbars": "^4.2.0",
32
+ "ember-cli-inject-live-reload": "^2.0.1",
32
33
  "ember-cli-sri": "^2.1.1",
33
34
  "ember-cli-template-lint": "^1.0.0-beta.3",
34
- "ember-cli-uglify": "^2.1.0",
35
- "ember-data": "~3.8.0",
36
- "ember-export-application-global": "^2.0.0",
35
+ "ember-cli-uglify": "^3.0.0",
36
+ "ember-data": "~3.15.0",
37
+ "ember-export-application-global": "^2.0.1",
38
+ "ember-fetch": "^7.0.0",
37
39
  "ember-font-awesome": "^4.0.0-rc.4",
38
- "ember-load-initializers": "^1.1.0",
40
+ "ember-load-initializers": "^2.1.1",
39
41
  "ember-maybe-import-regenerator": "^0.1.6",
40
- "ember-qunit": "^3.4.1",
41
- "ember-resolver": "^5.3.0",
42
- "ember-source": "^3.8.3",
43
- "ember-welcome-page": "^3.2.0",
44
- "eslint-plugin-ember": "^5.4.0",
45
- "jquery": "3.4.1",
42
+ "ember-qunit": "^4.6.0",
43
+ "ember-resolver": "^7.0.0",
44
+ "ember-source": "^3.15.0",
45
+ "eslint-plugin-ember": "^7.7.1",
46
+ "eslint-plugin-node": "^10.0.0",
46
47
  "loader.js": "^4.7.0",
47
48
  "prettier": "^1.19.1",
48
- "qunit-dom": "^0.8.5"
49
+ "qunit-dom": "^0.9.2"
49
50
  },
50
51
  "engines": {
51
- "node": "6.* || 8.* || >= 10.*"
52
+ "node": "8.* || >= 10.*"
53
+ },
54
+ "ember": {
55
+ "edition": "octane"
52
56
  },
53
57
  "dependencies": {
54
- "lodash": "^4.17.15",
55
58
  "moment": "~2.22.2"
56
59
  }
57
60
  }
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This script takes care of updating the content of the preloaded json in app/index.html and tests/index.html
4
+ # All you need to do is update the ruby hash of the corresponding file you want to update and run the script
5
+
6
+ require 'bundler/inline'
7
+ require 'json'
8
+ require 'cgi'
9
+
10
+ gemfile do
11
+ source 'https://rubygems.org'
12
+ gem 'nokogiri'
13
+ end
14
+
15
+ tests_index_html = {
16
+ env_expandable_keys: [],
17
+ gems_dir: "/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/",
18
+ backtrace_links_enabled: true,
19
+ gems_data: [
20
+ {
21
+ name: "activerecord",
22
+ url: "https://github.com/rails/rails/tree/v6.0.1/activerecord"
23
+ }
24
+ ],
25
+ directories: [
26
+ {
27
+ path: "/var/www/discourse",
28
+ url: "https://github.com/discourse/discourse",
29
+ main_app: true
30
+ },
31
+ {
32
+ path: "/var/www/discourse/plugins/discourse-prometheus",
33
+ url: "https://github.com/discourse/discourse-prometheus"
34
+ }
35
+ ],
36
+ application_version: "ce512452b512b909c38e9c63f2a0e1f8c17a2399"
37
+ }
38
+
39
+ app_index_html = {
40
+ env_expandable_keys: [],
41
+ patterns_enabled: true,
42
+ gems_dir: "/home/sam/.rbenv/versions/2.1.2.discourse/lib/ruby/gems/2.1.0/gems/",
43
+ backtrace_links_enabled: true,
44
+ gems_data: [],
45
+ directories: [
46
+ {
47
+ path: "/home/sam/Source/discourse",
48
+ url: "https://github.com/discourse/discourse",
49
+ main_app: true
50
+ }
51
+ ],
52
+ application_version: "b329e23f8511b7248c0e4aee370a9f8a249e1b84"
53
+ }
54
+
55
+ types = { app: app_index_html, tests: tests_index_html }
56
+
57
+ %i{app tests}.each do |type|
58
+ content = File.read("#{type}/index.html")
59
+ json = CGI.escapeHTML(JSON.generate(types[type]))
60
+ content.sub!(/data-preloaded=".*">$/, "data-preloaded=\"#{json}\">")
61
+ File.write("#{type}/index.html", content)
62
+ end
@@ -13,7 +13,6 @@ module.exports = {
13
13
  // --no-sandbox is needed when running Chrome inside a container
14
14
  process.env.CI ? '--no-sandbox' : null,
15
15
  '--headless',
16
- '--disable-gpu',
17
16
  '--disable-dev-shm-usage',
18
17
  '--disable-software-rasterizer',
19
18
  '--mute-audio',
@@ -6,7 +6,7 @@
6
6
  <title>ClientApp Tests</title>
7
7
  <meta name="description" content="">
8
8
  <meta name="viewport" content="width=device-width, initial-scale=1">
9
- <meta id="preloaded-data" data-root-path="/logs" data-preloaded="{&quot;env_expandable_keys&quot;:[]}">
9
+ <meta id="preloaded-data" data-root-path="/logs" data-preloaded="{&quot;env_expandable_keys&quot;:[],&quot;gems_dir&quot;:&quot;/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/&quot;,&quot;backtrace_links_enabled&quot;:true,&quot;gems_data&quot;:[{&quot;name&quot;:&quot;activerecord&quot;,&quot;url&quot;:&quot;https://github.com/rails/rails/tree/v6.0.1/activerecord&quot;}],&quot;directories&quot;:[{&quot;path&quot;:&quot;/var/www/discourse&quot;,&quot;url&quot;:&quot;https://github.com/discourse/discourse&quot;,&quot;main_app&quot;:true},{&quot;path&quot;:&quot;/var/www/discourse/plugins/discourse-prometheus&quot;,&quot;url&quot;:&quot;https://github.com/discourse/discourse-prometheus&quot;}],&quot;application_version&quot;:&quot;ce512452b512b909c38e9c63f2a0e1f8c17a2399&quot;}">
10
10
 
11
11
  {{content-for "head"}}
12
12
  {{content-for "test-head"}}
@@ -0,0 +1,109 @@
1
+ import { module, test } from "qunit";
2
+ import { setupRenderingTest } from "ember-qunit";
3
+ import { render, find, findAll } from "@ember/test-helpers";
4
+ import hbs from "htmlbars-inline-precompile";
5
+ import { uninitialize, mutatePreload } from "client-app/lib/preload";
6
+
7
+ module("Integration | Component | back-trace", function(hooks) {
8
+ setupRenderingTest(hooks);
9
+
10
+ hooks.beforeEach(function() {
11
+ uninitialize();
12
+ });
13
+
14
+ hooks.afterEach(function() {
15
+ uninitialize();
16
+ });
17
+
18
+ test("backtrace lines display and work correctly", async function(assert) {
19
+ const backtrace = `/var/www/discourse/vendor/bundle/ruby/2.6.0/gems/activerecord-6.0.1/lib/active_record/relation/finder_methods.rb:317:in \`exists?'
20
+ /var/www/discourse/lib/permalink_constraint.rb:6:in \`matches?'
21
+ /var/www/discourse/plugins/discourse-prometheus/lib/middleware/metrics.rb:17:in \`call'`;
22
+ this.set("backtrace", backtrace);
23
+ await render(hbs`{{back-trace backtrace=backtrace}}`);
24
+
25
+ const [gem, app, plugin] = findAll("a");
26
+ assert.equal(
27
+ gem.href,
28
+ "https://github.com/rails/rails/tree/v6.0.1/activerecord/lib/active_record/relation/finder_methods.rb#L317"
29
+ );
30
+
31
+ assert.equal(
32
+ app.href,
33
+ "https://github.com/discourse/discourse/blob/ce512452b512b909c38e9c63f2a0e1f8c17a2399/lib/permalink_constraint.rb#L6"
34
+ );
35
+
36
+ assert.equal(
37
+ plugin.href,
38
+ "https://github.com/discourse/discourse-prometheus/blob/master/lib/middleware/metrics.rb#L17"
39
+ );
40
+
41
+ let gemLine = find("div.backtrace-line");
42
+ assert.equal(
43
+ gemLine.textContent,
44
+ "activerecord-6.0.1/lib/active_record/relation/finder_methods.rb:317:in `exists?'",
45
+ "gem lines are truncated"
46
+ );
47
+ });
48
+
49
+ test("non-ruby backtraces don't break things", async function(assert) {
50
+ this.set(
51
+ "backtrace",
52
+ `m/<@https://discourse-cdn.com/assets/application-f59d2.br.js:1:27448
53
+ m@https://discourse-cdn.com/assets/application-f59d2.br.js:1:27560
54
+ string@https://discourse-cdn.com/assets/application-f59d2.br.js:1:27869`
55
+ );
56
+ await render(hbs`{{back-trace backtrace=backtrace}}`);
57
+ const lines = this.backtrace.split("\n");
58
+ findAll("div.backtrace-line").forEach((node, index) => {
59
+ assert.equal(node.textContent.trim(), lines[index]);
60
+ });
61
+ });
62
+
63
+ test("Github links use commit sha", async function(assert) {
64
+ const backtrace = `/var/www/discourse/lib/permalink_constraint.rb:6:in \`matches?'`;
65
+ let env = [
66
+ { application_version: "123abc" },
67
+ { application_version: "abc123" }
68
+ ];
69
+ this.setProperties({
70
+ backtrace,
71
+ env
72
+ });
73
+ await render(hbs`{{back-trace backtrace=backtrace env=env}}`);
74
+ let href = find("a").href;
75
+ assert.equal(
76
+ href,
77
+ "https://github.com/discourse/discourse/blob/123abc/lib/permalink_constraint.rb#L6",
78
+ "uses the first application_version if there are multiple versions"
79
+ );
80
+
81
+ env = { application_version: "567def" };
82
+ this.set("env", env);
83
+ await render(hbs`{{back-trace backtrace=backtrace env=env}}`);
84
+ href = find("a").href;
85
+ assert.equal(
86
+ href,
87
+ "https://github.com/discourse/discourse/blob/567def/lib/permalink_constraint.rb#L6",
88
+ "uses application_version when env is only a hash"
89
+ );
90
+
91
+ this.set("env", null);
92
+ await render(hbs`{{back-trace backtrace=backtrace env=env}}`);
93
+ href = find("a").href;
94
+ assert.equal(
95
+ href,
96
+ "https://github.com/discourse/discourse/blob/ce512452b512b909c38e9c63f2a0e1f8c17a2399/lib/permalink_constraint.rb#L6",
97
+ "falls back to preload if env doesn't contain application_version"
98
+ );
99
+
100
+ mutatePreload("application_version", null);
101
+ await render(hbs`{{back-trace backtrace=backtrace}}`);
102
+ href = find("a").href;
103
+ assert.equal(
104
+ href,
105
+ "https://github.com/discourse/discourse/blob/master/lib/permalink_constraint.rb#L6",
106
+ "falls back to master branch when neither preload nor application_version in env are available"
107
+ );
108
+ });
109
+ });
@@ -32,11 +32,12 @@ module("Integration | Component | message-info", function(hooks) {
32
32
  showTitle=showTitle
33
33
  currentEnvPosition=envPosition
34
34
  envChangedAction=callback
35
+ showShare=true
35
36
  actionsInMenu=actionsInMenu}}`
36
37
  );
37
38
  let activeTab = find(".message-info .content.active pre");
38
39
  assert.equal(
39
- activeTab.textContent,
40
+ activeTab.textContent.trim(),
40
41
  backtrace,
41
42
  "default active tab is backtrace"
42
43
  );
@@ -60,7 +61,7 @@ module("Integration | Component | message-info", function(hooks) {
60
61
  await click(find(".message-actions button.expand.no-text"));
61
62
  assert.equal(
62
63
  findAll(".actions-menu button").length,
63
- 2,
64
+ 3,
64
65
  "extra buttons shown inside a menu"
65
66
  );
66
67
  assert
@@ -85,7 +86,7 @@ module("Integration | Component | message-info", function(hooks) {
85
86
  .doesNotExist("menu expand button is not shown");
86
87
  assert.equal(
87
88
  findAll(".message-actions button").length,
88
- 3,
89
+ 4,
89
90
  "all actions buttons are shown inline when `actionsInMenu` is false"
90
91
  );
91
92
 
@@ -12,7 +12,9 @@ module("Integration | Component | patterns-list", function(hooks) {
12
12
  mutable: true,
13
13
  patterns: []
14
14
  });
15
- await render(hbs`{{patterns-list patterns=patterns mutable=mutable}}`);
15
+ await render(
16
+ hbs`{{patterns-list patterns=patterns mutable=mutable showCounter=true}}`
17
+ );
16
18
  assert
17
19
  .dom(".pattern-input")
18
20
  .exists("It shows an input when patterns are emtpy");
@@ -33,7 +35,10 @@ module("Integration | Component | patterns-list", function(hooks) {
33
35
  .doesNotExist("No save buttons are shown when there is 0 buffer");
34
36
  const counters = findAll("input.count");
35
37
  assert.equal(counters.length, 3, "counters shown for all patterns");
36
- assert.ok(counters.every(c => c.disabled), "counters are disabled");
38
+ assert.ok(
39
+ counters.every(c => c.disabled),
40
+ "counters are disabled"
41
+ );
37
42
 
38
43
  pattern1.set("count", 6);
39
44
  this.set("patterns", [pattern1, pattern2]);
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'logster/version'
3
4
  require 'logster/logger'
4
5
  require 'logster/message'
5
6
  require 'logster/configuration'
@@ -18,7 +18,7 @@ module Logster
18
18
  end
19
19
 
20
20
  # Modify the saved message to the given one (identified by message.key) and bump it to the top of the latest list
21
- def replace_and_bump(message, save_env: true)
21
+ def replace_and_bump(message)
22
22
  not_implemented
23
23
  end
24
24
 
@@ -195,26 +195,33 @@ module Logster
195
195
  similar = nil
196
196
 
197
197
  if Logster.config.allow_grouping
198
+ message.apply_message_size_limit(
199
+ Logster.config.maximum_message_size_bytes,
200
+ gems_dir: Logster.config.gems_dir
201
+ )
198
202
  key = self.similar_key(message)
199
203
  similar = get(key, load_env: false) if key
200
204
  end
201
205
 
206
+ message.drop_redundant_envs(Logster.config.max_env_count_per_message)
207
+ message.apply_env_size_limit(Logster.config.max_env_bytes)
208
+ saved = true
202
209
  if similar
203
- if similar.count < Logster::MAX_GROUPING_LENGTH
204
- similar.env = get_env(similar.key) || {}
205
- end
206
- save_env = similar.merge_similar_message(message)
207
-
208
- replace_and_bump(similar, save_env: save_env)
210
+ similar.merge_similar_message(message)
211
+ replace_and_bump(similar)
209
212
  similar
210
213
  else
211
- save message
214
+ message.apply_message_size_limit(
215
+ Logster.config.maximum_message_size_bytes,
216
+ gems_dir: Logster.config.gems_dir
217
+ )
218
+ saved = save(message)
212
219
  message
213
220
  end
214
221
 
215
222
  message = similar || message
216
223
 
217
- if Logster.config.enable_custom_patterns_via_ui || allow_custom_patterns
224
+ if (Logster.config.enable_custom_patterns_via_ui || allow_custom_patterns) && saved
218
225
  grouping_patterns = @patterns_cache.fetch(Logster::GroupingPattern::CACHE_KEY) do
219
226
  Logster::GroupingPattern.find_all(store: self)
220
227
  end
@@ -12,7 +12,12 @@ module Logster
12
12
  :environments,
13
13
  :rate_limit_error_reporting,
14
14
  :web_title,
15
- :maximum_message_size_bytes
15
+ :maximum_message_size_bytes,
16
+ :project_directories,
17
+ :enable_backtrace_links,
18
+ :gems_dir,
19
+ :max_env_bytes,
20
+ :max_env_count_per_message
16
21
  )
17
22
 
18
23
  attr_writer :subdirectory
@@ -26,7 +31,12 @@ module Logster
26
31
  @enable_custom_patterns_via_ui = false
27
32
  @rate_limit_error_reporting = true
28
33
  @enable_js_error_reporting = true
29
- @maximum_message_size_bytes = 60_000
34
+ @maximum_message_size_bytes = 10_000
35
+ @max_env_bytes = 1000
36
+ @max_env_count_per_message = 50
37
+ @project_directories = []
38
+ @enable_backtrace_links = true
39
+ @gems_dir = Gem.dir + "/gems/"
30
40
 
31
41
  @allow_grouping = false
32
42
 
@@ -7,7 +7,7 @@ module Logster
7
7
  private
8
8
 
9
9
  def report_to_store(severity, progname, message, opts = {})
10
- opts[:backtrace] ||= caller
10
+ opts[:backtrace] ||= caller.join("\n")
11
11
  Logster::Scheduler.schedule do
12
12
  super(severity, progname, message, opts)
13
13
  end
@@ -70,6 +70,18 @@ module Logster
70
70
  message = message.scrub
71
71
  end
72
72
 
73
+ # we want to get the backtrace as early as possible so that logster's
74
+ # own methods don't show up as the first few frames in the backtrace
75
+ if !opts || !opts.key?(:backtrace)
76
+ opts ||= {}
77
+ backtrace = caller_locations
78
+ while backtrace.first.path.end_with?("/logger.rb")
79
+ backtrace.shift
80
+ end
81
+ backtrace = backtrace.join("\n")
82
+ opts[:backtrace] = backtrace
83
+ end
84
+
73
85
  if @chained
74
86
  i = 0
75
87
  # micro optimise for logging
@@ -4,8 +4,6 @@ require 'digest/sha1'
4
4
  require 'securerandom'
5
5
 
6
6
  module Logster
7
-
8
- MAX_GROUPING_LENGTH = 50
9
7
  MAX_MESSAGE_LENGTH = 600
10
8
 
11
9
  class Message
@@ -22,9 +20,10 @@ module Logster
22
20
  hostname
23
21
  process_id
24
22
  application_version
23
+ time
25
24
  }
26
25
 
27
- attr_accessor :timestamp, :severity, :progname, :key, :backtrace, :count, :protected, :first_timestamp
26
+ attr_accessor :timestamp, :severity, :progname, :key, :backtrace, :count, :protected, :first_timestamp, :env_buffer
28
27
  attr_reader :message, :env
29
28
 
30
29
  def initialize(severity, progname, message, timestamp = nil, key = nil, count: 1)
@@ -37,6 +36,7 @@ module Logster
37
36
  @count = count || 1
38
37
  @protected = false
39
38
  @first_timestamp = nil
39
+ @env_buffer = []
40
40
  end
41
41
 
42
42
  def to_h(exclude_env: false)
@@ -82,7 +82,6 @@ module Logster
82
82
  end
83
83
 
84
84
  def env=(env)
85
- @env_json = nil
86
85
  @env = self.class.scrub_params(env)
87
86
  end
88
87
 
@@ -94,12 +93,18 @@ module Logster
94
93
  env ||= {}
95
94
  if Array === env
96
95
  env = env.map do |single_env|
97
- self.class.default_env.merge(single_env)
96
+ single_env = self.class.default_env.merge(single_env)
97
+ if !single_env.key?("time") && !single_env.key?(:time)
98
+ single_env["time"] = @timestamp || get_timestamp
99
+ end
100
+ single_env
98
101
  end
99
102
  else
100
103
  env = self.class.default_env.merge(env)
104
+ if !env.key?("time") && !env.key?(:time)
105
+ env["time"] = @timestamp || get_timestamp
106
+ end
101
107
  end
102
- @env_json = nil
103
108
  @env = Message.populate_from_env(env)
104
109
  end
105
110
 
@@ -127,12 +132,11 @@ module Logster
127
132
  if Array === env
128
133
  versions = env.map { |single_env| single_env["application_version"] }
129
134
  else
130
- versions = env["application_version"]
135
+ versions = [env["application_version"]]
131
136
  end
137
+ versions.compact!
132
138
 
133
- if versions && backtrace && backtrace.length > 0
134
- versions = [versions] if String === versions
135
-
139
+ if backtrace && backtrace.length > 0
136
140
  versions.map do |version|
137
141
  Digest::SHA1.hexdigest "#{version} #{backtrace}"
138
142
  end
@@ -146,31 +150,24 @@ module Logster
146
150
  def merge_similar_message(other)
147
151
  self.first_timestamp ||= self.timestamp
148
152
  self.timestamp = [self.timestamp, other.timestamp].max
149
-
150
153
  self.count += other.count || 1
151
- return false if self.count > Logster::MAX_GROUPING_LENGTH
152
154
 
153
- size = self.to_json(exclude_env: true).bytesize + self.env_json.bytesize
154
- extra_env_size = other.env_json.bytesize
155
- return false if size + extra_env_size > Logster.config.maximum_message_size_bytes
156
-
157
- other_env = JSON.load JSON.fast_generate other.env
158
- if Hash === other_env && !other_env.key?("time")
159
- other_env["time"] = other.timestamp
160
- end
161
- if Hash === self.env && !self.env.key?("time")
162
- self.env["time"] = self.first_timestamp
155
+ if Hash === other.env && !other.env.key?("time") && !other.env.key?(:time)
156
+ other.env["time"] = other.timestamp
163
157
  end
164
158
 
165
- if Array === self.env
166
- Array === other_env ? self.env.concat(other_env) : self.env << other_env
159
+ if Array === other.env
160
+ env_buffer.unshift(*other.env)
167
161
  else
168
- Array === other_env ? self.env = [self.env, *other_env] : self.env = [self.env, other_env]
162
+ env_buffer.unshift(other.env)
169
163
  end
170
- @env_json = nil
171
164
  true
172
165
  end
173
166
 
167
+ def has_env_buffer?
168
+ env_buffer.size > 0
169
+ end
170
+
174
171
  def self.populate_from_env(env)
175
172
  if Array === env
176
173
  env.map do |single_env|
@@ -229,10 +226,6 @@ module Logster
229
226
  end
230
227
  end
231
228
 
232
- def env_json
233
- @env_json ||= (self.env || {}).to_json
234
- end
235
-
236
229
  def self.scrub_params(params)
237
230
  if Array === params
238
231
  params.map! { |p| scrub_params(p) }
@@ -250,8 +243,74 @@ module Logster
250
243
  end
251
244
  end
252
245
 
246
+ def drop_redundant_envs(limit)
247
+ if Array === env
248
+ env.slice!(limit..-1)
249
+ end
250
+ end
251
+
252
+ def apply_env_size_limit(size_limit)
253
+ if Array === env
254
+ env.each { |e| truncate_env(e, size_limit) }
255
+ elsif Hash === env
256
+ truncate_env(env, size_limit)
257
+ end
258
+ end
259
+
260
+ def apply_message_size_limit(limit, gems_dir: nil)
261
+ size = self.to_json(exclude_env: true).bytesize
262
+ if size > limit && @backtrace
263
+ @backtrace.gsub!(gems_dir, "") if gems_dir
264
+ @backtrace.strip!
265
+ size = self.to_json(exclude_env: true).bytesize
266
+ backtrace_limit = limit - (size - @backtrace.bytesize)
267
+ return if backtrace_limit <= 0 || size <= limit
268
+ truncate_backtrace(backtrace_limit)
269
+ end
270
+ end
271
+
272
+ def truncate_backtrace(bytes_limit)
273
+ @backtrace = @backtrace.byteslice(0...bytes_limit)
274
+ while !@backtrace[-1].valid_encoding? && @backtrace.size > 1
275
+ @backtrace.slice!(-1)
276
+ end
277
+ end
278
+
253
279
  protected
254
280
 
281
+ def truncate_env(env, limit)
282
+ if JSON.fast_generate(env).bytesize > limit
283
+ sizes = {}
284
+ braces = '{}'.bytesize
285
+ env.each do |k, v|
286
+ sizes[k] = JSON.fast_generate(k => v).bytesize - braces
287
+ end
288
+ sorted = env.keys.sort { |a, b| sizes[a] <=> sizes[b] }
289
+
290
+ kept_keys = []
291
+ if env.key?(:time)
292
+ kept_keys << :time
293
+ elsif env.key?("time")
294
+ kept_keys << "time"
295
+ end
296
+
297
+ sum = braces
298
+ if time_key = kept_keys.first
299
+ sum += sizes[time_key]
300
+ sorted.delete(time_key)
301
+ end
302
+ comma = ','.bytesize
303
+
304
+ sorted.each do |k|
305
+ extra = kept_keys.size == 0 ? 0 : comma
306
+ break if sum + sizes[k] + extra > limit
307
+ kept_keys << k
308
+ sum += sizes[k] + extra
309
+ end
310
+ env.select! { |k| kept_keys.include?(k) }
311
+ end
312
+ end
313
+
255
314
  def truncate_message(msg)
256
315
  return msg unless msg
257
316
  msg = msg.inspect unless String === msg