onlylogs 0.10.0 → 0.11.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
  SHA256:
3
- metadata.gz: 9303c9ba4c727343d48702332baef85d112068f0639a148ff42afc8693b1f7af
4
- data.tar.gz: 82fe3f9d70cfc3b2d4a6f661a296974037aca9fd8bd8e97317476bb091d3c320
3
+ metadata.gz: 4707246899a958c58bf6ea58aba6f21234d938c660ce23dbad8df598654a4b5c
4
+ data.tar.gz: c46cf3df22e80cd90749bf144f62a271a9556b13ee718640377aa86f34608e05
5
5
  SHA512:
6
- metadata.gz: d7bf3497e6013d367776e50e716d113781cdde78b79d302b01a126778092aa36fb0f025c93162301b4f9444d9be83bd30ebd2b832d3e3e5da29d6ed721bd6138
7
- data.tar.gz: 879705e6eb4dafe29e1bf1698495cfb0731ccefc13f3127547a45173e4fdc6beda4ea53c13b4406ae6c3d1ba10ffe36f3b2f21bf1a7e3ff17adf195b5161170f
6
+ metadata.gz: 7eabe1b6124704eee89f759001cb7df9bf8fa5bb71ad98ac9c26bbc091db38c93ee3c7fdb4b35729f33cc9eeb2d0ac3170b2d2ef11ae5b979b7ec454664bc435
7
+ data.tar.gz: fa8226aa140b44b8eac0f3a03000aac95615470343d90de827d98da79ba4045c776b052171b3555ef955db2747dc760640cd787f6207ae95f6e67e70e152dca1
data/README.md CHANGED
@@ -58,6 +58,33 @@ Here you can grep your logs with regular expressions.
58
58
  > If ripgrep is not installed, onlylogs falls back to `grep`.
59
59
  > A warning icon (⚠️) will be displayed in the toolbar when using `grep` to indicate slower search performance.
60
60
 
61
+ ## Logging in production
62
+
63
+ Rails apps log to `STDOUT` in production and let the platform collect the stream.
64
+ onlylogs keeps that default: `Onlylogs::HttpLogger.new` writes every line to `$stdout` and, when
65
+ `ONLYLOGS_DRAIN_URL` is set, ships it to https://onlylogs.io as well. Nothing is written to disk.
66
+
67
+ ```ruby
68
+ # config/environments/production.rb
69
+ config.logger = Onlylogs::HttpLogger.new
70
+ ```
71
+
72
+ Keep this default on an **ephemeral filesystem** (Heroku, Deploio, Docker, Kubernetes). A log file
73
+ there is lost on every deploy and, while it grows, its page cache counts towards the container's
74
+ memory limit: it shows up as a memory leak that resets on each release.
75
+
76
+ If you have a **persistent disk** and want the files, for instance to browse them with the engine,
77
+ pass a rotating log device as the local fallback. Rails rotates at 100 MB in development, which is a
78
+ good size here too:
79
+
80
+ ```ruby
81
+ # config/environments/production.rb
82
+ log_file = Logger::LogDevice.new(Rails.root.join("log", "production.log"), shift_age: 5, shift_size: 100.megabytes)
83
+ config.logger = Onlylogs::HttpLogger.new(local_fallback: log_file)
84
+ ```
85
+
86
+ Any IO or log device works and is used as is. `Onlylogs::SocketLogger` accepts the same argument.
87
+
61
88
  ## Authentication
62
89
 
63
90
  Yes, we should do this right away, because this engine gives access to your log files, so you want to be sure.
@@ -294,7 +321,7 @@ The `Onlylogs::Formatter` supports a denylist: an array of regular expressions t
294
321
 
295
322
  ```ruby
296
323
  # config/environments/production.rb
297
- config.logger = Onlylogs::Logger.new(Rails.root.join("log", "production.log"))
324
+ config.logger = Onlylogs::HttpLogger.new
298
325
  config.logger.formatter.denylist = [/health_check/, /ping/, /\.css\z/]
299
326
  ```
300
327
 
@@ -92,6 +92,10 @@ module Onlylogs
92
92
  # an explicit cursor (matches the default whole-file live-mode page load).
93
93
  LIVE_TAIL_BYTES = 10_000
94
94
 
95
+ # The button re-runs the viewer's own reset, which reopens the file at its current tail.
96
+ ROTATED_MESSAGE = 'The file was rotated. <button type="button" class="reload-button" ' \
97
+ 'data-action="click->log-streamer#reset">Reload</button>'
98
+
95
99
  def start_log_watcher(file_path, filter = nil, regexp_mode = false)
96
100
  return if @log_watcher_running
97
101
 
@@ -130,6 +134,10 @@ module Onlylogs
130
134
  })
131
135
  end
132
136
  end
137
+
138
+ # watch returns on its own only when the file was rotated: nothing more will
139
+ # arrive under this name from where we are, so the viewer has to start over.
140
+ transmit({action: "finish", content: ROTATED_MESSAGE}) if @log_watcher_running
133
141
  end
134
142
  rescue => e
135
143
  Rails.logger.error "Log watcher error: #{e.message}"
@@ -7,6 +7,8 @@ export default class LogStreamerController extends Controller {
7
7
  autoScroll: { type: Boolean, default: true },
8
8
  filter: { type: String, default: '' },
9
9
  mode: { type: String, default: 'live' },
10
+ // false for a file nothing writes to any more: the viewer only searches, never tails.
11
+ liveEnabled: { type: Boolean, default: true },
10
12
  regexpMode: { type: Boolean, default: false },
11
13
  fileSize: { type: Number, default: 0 },
12
14
  startPosition: { type: Number, default: 0 },
@@ -125,7 +127,7 @@ export default class LogStreamerController extends Controller {
125
127
  }
126
128
 
127
129
  switchToLive() {
128
- if (this.isLiveMode()) return;
130
+ if (this.isLiveMode() || !this.liveEnabledValue) return;
129
131
 
130
132
  this.#clearHighlighting();
131
133
  this.#setMode('live');
@@ -178,8 +180,11 @@ export default class LogStreamerController extends Controller {
178
180
  // The only writer of modeValue. Everything that changes the mode goes through
179
181
  // here so the switch, the URL and the toolbar layout can never disagree.
180
182
  #setMode(mode) {
183
+ if (mode === 'live' && !this.liveEnabledValue) return;
184
+
181
185
  this.modeValue = mode;
182
- this.#updateUrlParam('mode', mode === 'live' ? null : 'static');
186
+ // Search is the only mode when live is disabled, so the URL need not say so.
187
+ this.#updateUrlParam('mode', mode === 'live' || !this.liveEnabledValue ? null : 'static');
183
188
  this.#liveFilterStartedAt = mode === 'live' ? new Date() : null;
184
189
  this.#syncModeControls();
185
190
  }
@@ -435,8 +440,9 @@ export default class LogStreamerController extends Controller {
435
440
  const end = endParam ? parseInt(endParam) : this.fileSizeValue;
436
441
  this.#setRange(start, end);
437
442
 
438
- // Calculate mode: check mode param, default to live
439
- this.modeValue = params.get('mode') === 'static' ? 'static' : 'live';
443
+ // Calculate mode: check mode param, default to live unless live is disabled
444
+ const wantsStatic = params.get('mode') === 'static' || !this.liveEnabledValue;
445
+ this.modeValue = wantsStatic ? 'static' : 'live';
440
446
  this.#liveFilterStartedAt = this.isLiveMode() ? new Date() : null;
441
447
  this.#syncModeControls();
442
448
  }
@@ -8,6 +8,7 @@ module Onlylogs
8
8
  self.path = path
9
9
  self.last_position = last_position
10
10
  validate!
11
+ @inode = ::File.stat(path).ino
11
12
  end
12
13
 
13
14
  def go_to_position(position)
@@ -16,11 +17,12 @@ module Onlylogs
16
17
  self.last_position = position
17
18
  end
18
19
 
20
+ # Yields new lines until the file is rotated away from under its name; then
21
+ # returns, since the position no longer means anything on the file now there.
19
22
  def watch(&block)
20
- # return enum_for(:watch) unless block
21
-
22
23
  loop do
23
24
  sleep 0.5
25
+ return if rotated?
24
26
 
25
27
  new_lines = read_new_lines
26
28
  next if new_lines.empty?
@@ -29,6 +31,15 @@ module Onlylogs
29
31
  end
30
32
  end
31
33
 
34
+ # A rename rotation puts another inode under the name, a copytruncate rotation
35
+ # keeps the inode but makes the file smaller than what was already read.
36
+ def rotated?
37
+ return true unless exist?
38
+
39
+ stat = ::File.stat(path)
40
+ stat.ino != @inode || stat.size < last_position
41
+ end
42
+
32
43
  def size
33
44
  ::File.size(path)
34
45
  end
@@ -1,4 +1,4 @@
1
- <%# locals: (log_file_path:) %>
1
+ <%# locals: (log_file_path:, live_enabled: true) %>
2
2
  <script nonce="<%= request.content_security_policy_nonce %>"
3
3
  src="<%= asset_path('onlylogs/clusterize.js') %>"></script>
4
4
  <link rel="stylesheet" href="<%= asset_path('onlylogs/clusterize.css') %>" />
@@ -8,7 +8,9 @@
8
8
  autoscroll = local_assigns[:autoscroll] != false
9
9
  filter = local_assigns[:filter] || ""
10
10
  regexp_mode = local_assigns[:regexp_mode] == true
11
- mode = local_assigns[:mode] || "live"
11
+ # A file nothing writes to any more (no drain attached, a rotated log) has nothing
12
+ # to follow, so live mode is not offered at all and the viewer opens in search.
13
+ mode = live_enabled ? (local_assigns[:mode] || "live") : "static"
12
14
  file_size = File.size(log_file_path)
13
15
  start_position = local_assigns[:start_position]
14
16
  end_position = local_assigns[:end_position]
@@ -27,6 +29,7 @@
27
29
  data-log-streamer-auto-scroll-value="<%= autoscroll %>"
28
30
  data-log-streamer-regexp-mode-value="<%= regexp_mode %>"
29
31
  data-log-streamer-mode-value="<%= mode %>"
32
+ data-log-streamer-live-enabled-value="<%= live_enabled %>"
30
33
  data-log-streamer-file-size-value="<%= file_size %>"
31
34
  data-action="text-selection:start->log-streamer#pauseForSelection text-selection:search->log-streamer#switchToSearch"
32
35
  class="onlylogs-log-container" >
@@ -39,7 +42,13 @@
39
42
  at all until something is typed. This says so instead of looking idle. %>
40
43
  <div class="onlylogs-search-placeholder" data-log-streamer-target="searchPlaceholder" hidden>
41
44
  <p class="onlylogs-search-placeholder__title">Nothing to search for yet</p>
42
- <p class="onlylogs-search-placeholder__hint">Type a query to search this file, or switch to Live to follow it as it is written.</p>
45
+ <p class="onlylogs-search-placeholder__hint">
46
+ <% if live_enabled %>
47
+ Type a query to search this file, or switch to Live to follow it as it is written.
48
+ <% else %>
49
+ Type a query to search this file.
50
+ <% end %>
51
+ </p>
43
52
  </div>
44
53
 
45
54
  <button type="button"
@@ -52,6 +61,7 @@
52
61
 
53
62
  <div class="onlylogs-log-toolbar">
54
63
  <div class="onlylogs-toolbar-row onlylogs-toolbar-row--controls">
64
+ <% if live_enabled %>
55
65
  <div class="onlylogs-mode-switch" role="group" aria-label="Log mode">
56
66
  <button type="button"
57
67
  class="onlylogs-mode-switch__button"
@@ -74,6 +84,7 @@
74
84
  <span>🔍 <u>S</u>earch</span>
75
85
  </button>
76
86
  </div>
87
+ <% end %>
77
88
  <div>
78
89
  <label class="toolbar-label">
79
90
  <span data-onlylogs-mode="live">Follow:</span>
@@ -242,7 +242,8 @@
242
242
  background-color: var(--ol-accent);
243
243
  }
244
244
 
245
- .search-whole-file-button {
245
+ .search-whole-file-button,
246
+ .reload-button {
246
247
  font: inherit;
247
248
  border: 1px solid var(--ol-accent-border);
248
249
  background-color: var(--ol-accent-soft);
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: onlylogs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi