onlylogs 0.10.0 → 0.12.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +270 -69
  3. data/app/assets/images/onlylogs/favicon/apple-touch-icon.png +0 -0
  4. data/app/assets/images/onlylogs/favicon/favicon-96x96.png +0 -0
  5. data/app/assets/images/onlylogs/favicon/favicon.ico +0 -0
  6. data/app/assets/images/onlylogs/favicon/favicon.svg +1 -3
  7. data/app/assets/images/onlylogs/favicon/web-app-manifest-192x192.png +0 -0
  8. data/app/assets/images/onlylogs/favicon/web-app-manifest-512x512.png +0 -0
  9. data/app/assets/images/onlylogs/logo.png +0 -0
  10. data/app/assets/javascripts/onlylogs/clusterize.js +26 -1
  11. data/app/channels/onlylogs/logs_channel.rb +22 -9
  12. data/app/controllers/concerns/onlylogs/authentication.rb +26 -0
  13. data/app/controllers/onlylogs/application_controller.rb +1 -18
  14. data/app/controllers/onlylogs/queries_controller.rb +70 -0
  15. data/app/controllers/onlylogs/viewer_controller.rb +7 -0
  16. data/app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js +131 -21
  17. data/app/javascript/onlylogs/controllers/log_cursor_controller.js +286 -0
  18. data/app/javascript/onlylogs/controllers/log_streamer_controller.js +52 -6
  19. data/app/javascript/onlylogs/controllers/match_highlight_controller.js +82 -0
  20. data/app/javascript/onlylogs/controllers/queries_controller.js +470 -0
  21. data/app/javascript/onlylogs/controllers/range_slider_controller.js +5 -4
  22. data/app/javascript/onlylogs/controllers/text_selection_controller.js +136 -15
  23. data/app/models/onlylogs/file.rb +27 -7
  24. data/app/models/onlylogs/grep.rb +13 -6
  25. data/app/models/onlylogs/queries_record.rb +28 -0
  26. data/app/models/onlylogs/query.rb +29 -0
  27. data/app/views/onlylogs/logs/index.html.erb +1 -1
  28. data/app/views/onlylogs/shared/_log_container.html.erb +179 -30
  29. data/app/views/onlylogs/shared/_log_container_styles.html.erb +731 -45
  30. data/app/views/onlylogs/shared/_shortcuts_dialog.html.erb +62 -0
  31. data/bin/super_grep +7 -0
  32. data/bin/super_ripgrep +5 -0
  33. data/config/routes.rb +1 -0
  34. data/lib/onlylogs/configuration.rb +14 -1
  35. data/lib/onlylogs/http_device.rb +28 -13
  36. data/lib/onlylogs/version.rb +1 -1
  37. data/lib/onlylogs.rb +4 -0
  38. metadata +38 -1
@@ -0,0 +1,62 @@
1
+ <%# locals: (modifier_label:) %>
2
+ <%
3
+ sections = [
4
+ ["Hold #{modifier_label} and press – anywhere, also while typing", [
5
+ [%w[L], "Live"],
6
+ [%w[S], "Search"],
7
+ [%w[Q], "Go to the query"],
8
+ [%w[X], "Clear the query"],
9
+ [%w[V], "Saved searches: apply, rename or delete one"],
10
+ [%w[F], "Save the query (the star)"],
11
+ [%w[R], "Regexp on/off"],
12
+ [%w[I], "Ignore case on/off"],
13
+ [%w[A], "Autoscroll on/off (live)"],
14
+ [%w[W], "Search the whole file (live)"],
15
+ [%w[G], "Range slider (search), then ← →"],
16
+ [%w[0], "Reset the range (search)"],
17
+ [%w[.], "Stop the search"],
18
+ [%w[C], "Clear the lines"],
19
+ [%w[J], "Jump into the lines"]
20
+ ]],
21
+ ["When not typing", [
22
+ [%w[/], "Go to the query"],
23
+ [%w[↓ j], "Jump into the lines"],
24
+ [%w[?], "This help"]
25
+ ]],
26
+ ["In the query", [
27
+ [%w[Enter Esc], "Jump into the lines"]
28
+ ]],
29
+ ["In the lines", [
30
+ [%w[↑ ↓ k j], "Previous / next line"],
31
+ [%w[PgUp PgDn], "A page up / down"],
32
+ [%w[Home End g G], "First / last line"],
33
+ [%w[← →], "Select the previous / next word"],
34
+ [["Shift ← →"], "Grow the selection by a word"],
35
+ [%w[Enter], "Search the selection, or show around this line"],
36
+ [%w[y], "Copy the line"],
37
+ [%w[Esc], "Clear the selection, then leave the lines"]
38
+ ]]
39
+ ]
40
+ %>
41
+ <dialog class="onlylogs-shortcuts-dialog" data-keyboard-shortcuts-target="dialog" closedby="any"
42
+ aria-labelledby="onlylogs-shortcuts-title">
43
+ <form method="dialog" class="onlylogs-shortcuts-dialog__header">
44
+ <h2 id="onlylogs-shortcuts-title">Keyboard shortcuts</h2>
45
+ <button aria-label="Close" autofocus>×</button>
46
+ </form>
47
+ <div class="onlylogs-shortcuts-dialog__sections">
48
+ <% sections.each do |title, shortcuts| %>
49
+ <section>
50
+ <h3><%= title %></h3>
51
+ <dl>
52
+ <% shortcuts.each do |keys, description| %>
53
+ <div>
54
+ <dt><% keys.each do |key| %><kbd><%= key %></kbd><% end %></dt>
55
+ <dd><%= description %></dd>
56
+ </div>
57
+ <% end %>
58
+ </dl>
59
+ </section>
60
+ <% end %>
61
+ </div>
62
+ </dialog>
data/bin/super_grep CHANGED
@@ -20,6 +20,10 @@ while [[ $# -gt 0 ]]; do
20
20
  end_position="$2"
21
21
  shift 2
22
22
  ;;
23
+ --case-insensitive)
24
+ case_insensitive=true
25
+ shift
26
+ ;;
23
27
  --regexp)
24
28
  regexp_mode=true
25
29
  shift
@@ -60,6 +64,9 @@ grep_cmd=(grep -E -h) # -h = no filename (portable)
60
64
  if [ -n "$max_matches" ]; then
61
65
  grep_cmd+=(-m "$max_matches") # -m = max count (portable)
62
66
  fi
67
+ if [ "$case_insensitive" = true ]; then
68
+ grep_cmd+=(-i)
69
+ fi
63
70
 
64
71
  # Handle byte range if specified
65
72
  if [ -n "$start_position" ] || [ -n "$end_position" ]; then
data/bin/super_ripgrep CHANGED
@@ -20,6 +20,10 @@ while [[ $# -gt 0 ]]; do
20
20
  end_position="$2"
21
21
  shift 2
22
22
  ;;
23
+ --case-insensitive)
24
+ case_insensitive=true
25
+ shift
26
+ ;;
23
27
  --regexp)
24
28
  regexp_mode=true
25
29
  shift
@@ -62,6 +66,7 @@ query_regex="${query_regex//$placeholder/$actual_color_regex}"
62
66
  # --byte-offset: include byte offset in output for the logs-around-line feature
63
67
  rg_cmd="rg --color=never --no-filename --byte-offset --no-mmap"
64
68
  [ -n "$max_matches" ] && rg_cmd="$rg_cmd --max-count=$max_matches"
69
+ [ "$case_insensitive" = true ] && rg_cmd="$rg_cmd --ignore-case"
65
70
 
66
71
  # Handle byte range if specified
67
72
  if [ -n "$start_position" ] || [ -n "$end_position" ]; then
data/config/routes.rb CHANGED
@@ -2,4 +2,5 @@ Onlylogs::Engine.routes.draw do
2
2
  root "logs#index"
3
3
  resources :logs, only: [:index]
4
4
  get "download", to: "logs#download", as: :download_log
5
+ resources :queries, only: [:index, :create, :show, :update, :destroy]
5
6
  end
@@ -4,7 +4,7 @@ module Onlylogs
4
4
  class Configuration
5
5
  attr_accessor :log_file_patterns, :default_log_file_path, :basic_auth_user, :basic_auth_password,
6
6
  :parent_controller, :disable_basic_authentication, :ripgrep_enabled, :editor,
7
- :max_line_matches, :search_timeout
7
+ :max_line_matches, :search_timeout, :database_dir, :viewer_parent_controller, :authorize_log_file
8
8
 
9
9
  # Seconds a viewer search may run before it is stopped. Bounded by default:
10
10
  # a search holds a CPU for as long as it runs, and an unbounded one over a
@@ -19,11 +19,14 @@ module Onlylogs
19
19
  @basic_auth_user = default_basic_auth_user
20
20
  @basic_auth_password = default_basic_auth_password
21
21
  @parent_controller = nil
22
+ @viewer_parent_controller = nil
23
+ @authorize_log_file = nil
22
24
  @disable_basic_authentication = false
23
25
  @ripgrep_enabled = default_ripgrep_enabled
24
26
  @editor = nil
25
27
  @max_line_matches = 100000
26
28
  @search_timeout = DEFAULT_SEARCH_TIMEOUT
29
+ @database_dir = ".onlylogs"
27
30
  end
28
31
 
29
32
  def configure
@@ -129,6 +132,16 @@ module Onlylogs
129
132
  configuration.parent_controller
130
133
  end
131
134
 
135
+ def self.viewer_parent_controller
136
+ configuration.viewer_parent_controller
137
+ end
138
+
139
+ # Runs in the controller, so it can reach the host's current user.
140
+ def self.log_file_authorized?(controller, file_path)
141
+ hook = configuration.authorize_log_file
142
+ hook.nil? || controller.instance_exec(file_path, &hook).present?
143
+ end
144
+
132
145
  def self.disable_basic_authentication?
133
146
  configuration.disable_basic_authentication
134
147
  end
@@ -7,15 +7,17 @@ require_relative "spool"
7
7
  # A Logger log device that sends log lines to onlylogs.io (or any Vector-compatible sink) directly
8
8
  # via HTTP.
9
9
  #
10
+ # Delivery is fire and forget: a batch the drain does not accept within the timeouts is lost.
10
11
  # When the drain is unreachable or unresponsive, we do two things to protect the app:
11
12
  # * an upper bound to the in-memory queue: log lines can never accumulate without limit and exhaust memory
12
13
  # * cooldown: once the drain is known to be failing we stop attempting
13
14
  # requests for a cooldown period instead of blocking on every send for the full
14
15
  # read timeout (a down host accepts the TCP/TLS connection but never answers).
15
16
  #
16
- # By default an on-disk Spool buffers any batch we could not deliver and replays it once the
17
- # drain recovers, so a transient outage or a restart does not lose logs. It is on by default
18
- # (set ONLYLOGS_SPOOL_DIR empty to disable) and bounded by bytes; see Onlylogs::Spool.
17
+ # ONLYLOGS_SPOOL_ENABLED=true (also 1, yes or on, in any case) opts into an on-disk Spool that
18
+ # buffers any batch we could not deliver and replays it once the drain recovers, so a transient
19
+ # outage or a restart does not lose logs. It lives in ONLYLOGS_SPOOL_DIR and is bounded by bytes;
20
+ # see Onlylogs::Spool.
19
21
  #
20
22
  # Every write checks that a sender thread is alive in the current process and starts one if not:
21
23
  # * The device is usually built in the Puma master (production.rb runs before the workers are
@@ -28,11 +30,12 @@ require_relative "spool"
28
30
  #
29
31
  # Not every failure is worth retrying. The drain's answer decides what happens to a batch:
30
32
  # * 2xx: delivered.
31
- # * 429: the drain is overloaded and asks us to slow down. Pause for Retry-After (or a cooldown)
32
- # and keep the batch on disk; nothing is lost, it is just late.
33
+ # * 429: the drain is overloaded and asks us to slow down. Pause for Retry-After (or a cooldown);
34
+ # with a spool the batch is kept on disk and is just late, without one it is lost.
33
35
  # * other 4xx: the drain will never accept this batch (unknown token, paused project, body too
34
36
  # big). Retrying cannot help, so the batch is dropped and a warning says why.
35
- # * 5xx, timeouts, connection errors: retryable. Count towards opening the circuit and spool.
37
+ # * 5xx, timeouts, connection errors: an outage. Count towards opening the circuit; the batch
38
+ # goes to the spool if there is one.
36
39
  module Onlylogs
37
40
  class HttpDevice
38
41
  # The drain answered with a 4xx other than 429: the batch itself is the problem, not the drain.
@@ -85,6 +88,7 @@ module Onlylogs
85
88
  read_timeout: ENV.fetch("ONLYLOGS_READ_TIMEOUT", DEFAULT_READ_TIMEOUT),
86
89
  circuit_cooldown: ENV.fetch("ONLYLOGS_CIRCUIT_COOLDOWN", CIRCUIT_COOLDOWN),
87
90
  keep_alive_timeout: ENV.fetch("ONLYLOGS_KEEP_ALIVE_TIMEOUT", DEFAULT_KEEP_ALIVE_TIMEOUT),
91
+ spool_enabled: ENV.fetch("ONLYLOGS_SPOOL_ENABLED", false),
88
92
  spool_dir: ENV.fetch("ONLYLOGS_SPOOL_DIR", default_spool_dir),
89
93
  spool_max_bytes: ENV.fetch("ONLYLOGS_SPOOL_MAX_BYTES", Spool::DEFAULT_MAX_BYTES)
90
94
  )
@@ -99,6 +103,7 @@ module Onlylogs
99
103
  @read_timeout = float_setting("ONLYLOGS_READ_TIMEOUT", read_timeout, DEFAULT_READ_TIMEOUT)
100
104
  @circuit_cooldown = float_setting("ONLYLOGS_CIRCUIT_COOLDOWN", circuit_cooldown, CIRCUIT_COOLDOWN)
101
105
  @keep_alive_timeout = float_setting("ONLYLOGS_KEEP_ALIVE_TIMEOUT", keep_alive_timeout, DEFAULT_KEEP_ALIVE_TIMEOUT)
106
+ @spool_enabled = boolean_setting("ONLYLOGS_SPOOL_ENABLED", spool_enabled, false)
102
107
  @spool_dir = spool_dir
103
108
  @spool_max_bytes = integer_setting("ONLYLOGS_SPOOL_MAX_BYTES", spool_max_bytes, Spool::DEFAULT_MAX_BYTES)
104
109
  @supervisor_mutex = Mutex.new
@@ -159,6 +164,17 @@ module Onlylogs
159
164
  value.nil? || value.to_s.strip.empty?
160
165
  end
161
166
 
167
+ TRUTHY = %w[true 1 yes on].freeze
168
+ FALSY = %w[false 0 no off].freeze
169
+
170
+ def boolean_setting(name, value, default)
171
+ text = value.to_s.strip.downcase
172
+ return true if TRUTHY.include?(text)
173
+ return false if text.empty? || FALSY.include?(text)
174
+
175
+ fallback_setting(name, value, default, "true or false")
176
+ end
177
+
162
178
  def integer_setting(name, value, default, min: 1)
163
179
  number = Integer(value, exception: false)
164
180
  return number if number && number >= min
@@ -230,7 +246,7 @@ module Onlylogs
230
246
  @http_mutex = Mutex.new
231
247
  @http = nil
232
248
  @sender_thread = nil
233
- @spool = build_spool(@spool_dir, @spool_max_bytes) if @drain_url
249
+ @spool = build_spool(@spool_dir, @spool_max_bytes) if @drain_url && @spool_enabled
234
250
  @consecutive_failures = 0
235
251
  @circuit_open_until = nil
236
252
  @dropped = 0
@@ -326,8 +342,7 @@ module Onlylogs
326
342
  body = lines.join("\n")
327
343
 
328
344
  # Drain is known to be down: skip the request entirely so we don't block for the full read
329
- # timeout on every batch. Buffer the batch so the cooldown does not cost us data (without a
330
- # spool configured, spool_write is a no-op and the batch is dropped — best-effort logging).
345
+ # timeout on every batch. The batch is dropped, unless a spool is configured to keep it.
331
346
  if circuit_open?
332
347
  spool_write(body)
333
348
  return
@@ -373,8 +388,9 @@ module Onlylogs
373
388
  end
374
389
  end
375
390
 
391
+ # A spool that cannot be set up must not stop the app from logging: it is disabled with a warning.
376
392
  def build_spool(dir, max_bytes)
377
- return if dir.nil? || dir.to_s.strip.empty?
393
+ raise ArgumentError, "ONLYLOGS_SPOOL_DIR is empty" if blank?(dir)
378
394
 
379
395
  Spool.new(dir: dir, max_bytes: max_bytes)
380
396
  rescue => e
@@ -382,9 +398,8 @@ module Onlylogs
382
398
  nil
383
399
  end
384
400
 
385
- # The spool is on by default. It lives under the app's tmp dir, which survives a drain outage
386
- # while the app keeps running; point ONLYLOGS_SPOOL_DIR at a persistent volume to also survive
387
- # redeploys, or set it empty to disable.
401
+ # Under the app's tmp dir, which survives a drain outage while the app keeps running; point
402
+ # ONLYLOGS_SPOOL_DIR at a persistent volume to also survive redeploys.
388
403
  def default_spool_dir
389
404
  base = if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
390
405
  Rails.root.to_s
@@ -1,3 +1,3 @@
1
1
  module Onlylogs
2
- VERSION = "0.10.0"
2
+ VERSION = "0.12.0"
3
3
  end
data/lib/onlylogs.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require "sqlite_shards"
1
2
  require "onlylogs/version"
2
3
  require "onlylogs/configuration"
3
4
  require "onlylogs/engine"
@@ -18,6 +19,9 @@ require "onlylogs/http_logger"
18
19
  # loader.setup
19
20
 
20
21
  module Onlylogs
22
+ # Not autoloaded: referenced from class bodies (rescue_from, subclassing).
23
+ class Error < StandardError; end
24
+
21
25
  class ForbiddenPathError < StandardError; end
22
26
 
23
27
  if defined?(Importmap)
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.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Rodi
@@ -23,6 +23,34 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '8.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: sqlite3
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sqlite_shards
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
26
54
  description: This gem includes all the tools needed to view and stream you log files
27
55
  directly on a web interface.
28
56
  email:
@@ -49,15 +77,21 @@ files:
49
77
  - app/assets/stylesheets/onlylogs/clusterize.css
50
78
  - app/channels/onlylogs/application_cable/channel.rb
51
79
  - app/channels/onlylogs/logs_channel.rb
80
+ - app/controllers/concerns/onlylogs/authentication.rb
52
81
  - app/controllers/onlylogs/application_controller.rb
53
82
  - app/controllers/onlylogs/logs_controller.rb
83
+ - app/controllers/onlylogs/queries_controller.rb
84
+ - app/controllers/onlylogs/viewer_controller.rb
54
85
  - app/helpers/onlylogs/application_helper.rb
55
86
  - app/javascript/onlylogs/application.js
56
87
  - app/javascript/onlylogs/controllers/application.js
57
88
  - app/javascript/onlylogs/controllers/auto_submit_controller.js
58
89
  - app/javascript/onlylogs/controllers/index.js
59
90
  - app/javascript/onlylogs/controllers/keyboard_shortcuts_controller.js
91
+ - app/javascript/onlylogs/controllers/log_cursor_controller.js
60
92
  - app/javascript/onlylogs/controllers/log_streamer_controller.js
93
+ - app/javascript/onlylogs/controllers/match_highlight_controller.js
94
+ - app/javascript/onlylogs/controllers/queries_controller.js
61
95
  - app/javascript/onlylogs/controllers/range_slider_controller.js
62
96
  - app/javascript/onlylogs/controllers/text_selection_controller.js
63
97
  - app/jobs/onlylogs/application_job.rb
@@ -66,6 +100,8 @@ files:
66
100
  - app/models/onlylogs/file.rb
67
101
  - app/models/onlylogs/file_path_parser.rb
68
102
  - app/models/onlylogs/grep.rb
103
+ - app/models/onlylogs/queries_record.rb
104
+ - app/models/onlylogs/query.rb
69
105
  - app/models/onlylogs/secure_file_path.rb
70
106
  - app/views/home/show.html.erb
71
107
  - app/views/layouts/onlylogs/application.html.erb
@@ -73,6 +109,7 @@ files:
73
109
  - app/views/onlylogs/shared/_log_container.html.erb
74
110
  - app/views/onlylogs/shared/_log_container_styles.html.erb
75
111
  - app/views/onlylogs/shared/_range_slider.html.erb
112
+ - app/views/onlylogs/shared/_shortcuts_dialog.html.erb
76
113
  - bin/onlylogs_sidecar
77
114
  - bin/super_grep
78
115
  - bin/super_ripgrep