onlylogs 0.3.1 → 0.4.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 +4 -4
- data/README.md +2 -2
- data/app/channels/onlylogs/logs_channel.rb +15 -15
- data/app/controllers/onlylogs/application_controller.rb +1 -1
- data/app/controllers/onlylogs/logs_controller.rb +18 -3
- data/app/models/onlylogs/ansi_color_parser.rb +13 -13
- data/app/models/onlylogs/file_path_parser.rb +29 -26
- data/app/models/onlylogs/grep.rb +3 -3
- data/app/models/onlylogs/secure_file_path.rb +1 -3
- data/app/views/onlylogs/logs/index.html.erb +41 -1
- data/app/views/onlylogs/shared/_log_container.html.erb +1 -1
- data/config/routes.rb +1 -1
- data/lib/onlylogs/configuration.rb +48 -9
- data/lib/onlylogs/engine.rb +1 -1
- data/lib/onlylogs/formatter.rb +1 -1
- data/lib/onlylogs/socket_logger.rb +9 -6
- data/lib/onlylogs/version.rb +1 -1
- data/lib/puma/plugin/onlylogs_sidecar.rb +9 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37909543bef9f932a762a08b53c0dfbebe9094f41e96d0de8740e1e1b4b66614
|
|
4
|
+
data.tar.gz: 1abced159b2385cd445b05196a6ac47801aeeaac96c8062729251aaf778733bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4def45bcdb6d9c301f3cb2d65de39f2e27247a4ae5fd7835325e8f80781279ce7ed5ac280fea95cf7d61af2d9713fae008485f9e702f4ab747a151712340bf56
|
|
7
|
+
data.tar.gz: 74e0fa68229359024780f850dd2df95ab1d930b6a949ec85b63f20d7e8ff1253e79da81f384bb6057d588b10d67764dbaa7761db1212051c8e69c2841a8dce89
|
data/README.md
CHANGED
|
@@ -174,7 +174,7 @@ You can configure which files onlylogs is allowed to access by creating a config
|
|
|
174
174
|
```ruby
|
|
175
175
|
# config/initializers/onlylogs.rb
|
|
176
176
|
Onlylogs.configure do |config|
|
|
177
|
-
config.
|
|
177
|
+
config.log_file_patterns = [
|
|
178
178
|
# Default Rails log files
|
|
179
179
|
Rails.root.join("log/development.log"),
|
|
180
180
|
Rails.root.join("log/production.log"),
|
|
@@ -206,7 +206,7 @@ Onlylogs supports glob patterns to allow multiple files at once:
|
|
|
206
206
|
```ruby
|
|
207
207
|
# config/initializers/onlylogs.rb
|
|
208
208
|
Onlylogs.configure do |config|
|
|
209
|
-
config.
|
|
209
|
+
config.log_file_patterns = [
|
|
210
210
|
# Allow all .log files in the log directory
|
|
211
211
|
Rails.root.join("log/*.log"),
|
|
212
212
|
|
|
@@ -18,9 +18,9 @@ module Onlylogs
|
|
|
18
18
|
file_path = Onlylogs::SecureFilePath.decrypt(encrypted_file_path)
|
|
19
19
|
|
|
20
20
|
# Verify the decrypted path is still allowed
|
|
21
|
-
unless Onlylogs.
|
|
21
|
+
unless Onlylogs.file_path_permitted?(file_path)
|
|
22
22
|
Rails.logger.error "Onlylogs: Attempted to access non-allowed file: #{file_path}"
|
|
23
|
-
transmit({
|
|
23
|
+
transmit({action: "error", content: "Access denied"})
|
|
24
24
|
return
|
|
25
25
|
end
|
|
26
26
|
else
|
|
@@ -29,13 +29,13 @@ module Onlylogs
|
|
|
29
29
|
end
|
|
30
30
|
rescue Onlylogs::SecureFilePath::SecurityError => e
|
|
31
31
|
Rails.logger.error "Onlylogs: Security violation - #{e.message}"
|
|
32
|
-
transmit({
|
|
32
|
+
transmit({action: "error", content: "Access denied"})
|
|
33
33
|
return
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
# Check if the file is a text file
|
|
37
37
|
unless Onlylogs::File.text_file?(file_path)
|
|
38
|
-
transmit({
|
|
38
|
+
transmit({action: "error", content: "Cannot read file: File is not a text file"})
|
|
39
39
|
return
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -57,7 +57,7 @@ module Onlylogs
|
|
|
57
57
|
|
|
58
58
|
def stop_watcher
|
|
59
59
|
cleanup_existing_operations
|
|
60
|
-
transmit({
|
|
60
|
+
transmit({action: "finish", content: "Search stopped."})
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
def unsubscribed
|
|
@@ -82,11 +82,11 @@ module Onlylogs
|
|
|
82
82
|
@filter = filter
|
|
83
83
|
@regexp_mode = regexp_mode
|
|
84
84
|
|
|
85
|
-
transmit({
|
|
85
|
+
transmit({action: "message", content: "Reading file. Please wait..."})
|
|
86
86
|
|
|
87
87
|
@log_file = Onlylogs::File.new(file_path, last_position: cursor_position)
|
|
88
88
|
|
|
89
|
-
transmit({
|
|
89
|
+
transmit({action: "message", content: ""})
|
|
90
90
|
|
|
91
91
|
@log_watcher_thread = Thread.new do
|
|
92
92
|
Rails.logger.silence(Logger::ERROR) do
|
|
@@ -107,13 +107,13 @@ module Onlylogs
|
|
|
107
107
|
|
|
108
108
|
if lines_to_send.any?
|
|
109
109
|
transmit({
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
action: "append_logs",
|
|
111
|
+
lines: lines_to_send
|
|
112
|
+
})
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
|
-
rescue
|
|
116
|
+
rescue => e
|
|
117
117
|
Rails.logger.error "Log watcher error: #{e.message}"
|
|
118
118
|
Rails.logger.error e.backtrace.join("\n")
|
|
119
119
|
ensure
|
|
@@ -147,7 +147,7 @@ module Onlylogs
|
|
|
147
147
|
@log_watcher_running = true
|
|
148
148
|
@log_file = Onlylogs::File.new(file_path, last_position: 0)
|
|
149
149
|
|
|
150
|
-
transmit({
|
|
150
|
+
transmit({action: "message", content: "Searching..."})
|
|
151
151
|
|
|
152
152
|
@batch_sender = BatchSender.new(self)
|
|
153
153
|
@batch_sender.start
|
|
@@ -157,7 +157,7 @@ module Onlylogs
|
|
|
157
157
|
begin
|
|
158
158
|
Rails.logger.silence(Logger::ERROR) do
|
|
159
159
|
@log_file.grep(filter, regexp_mode: regexp_mode, start_position: start_position, end_position: end_position) do |log_line|
|
|
160
|
-
|
|
160
|
+
break if @batch_sender.nil?
|
|
161
161
|
|
|
162
162
|
# Add to batch buffer (sender thread will handle sending)
|
|
163
163
|
@batch_sender.add_line(render_log_line(log_line))
|
|
@@ -171,9 +171,9 @@ module Onlylogs
|
|
|
171
171
|
|
|
172
172
|
# Send completion message
|
|
173
173
|
if line_count >= Onlylogs.max_line_matches
|
|
174
|
-
transmit({
|
|
174
|
+
transmit({action: "finish", content: "Search finished. Search results limit reached."})
|
|
175
175
|
else
|
|
176
|
-
transmit({
|
|
176
|
+
transmit({action: "finish", content: "Search finished."})
|
|
177
177
|
end
|
|
178
178
|
ensure
|
|
179
179
|
# Always cleanup even if interrupted or error occurs
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module Onlylogs
|
|
2
|
-
class ApplicationController <
|
|
2
|
+
class ApplicationController < Onlylogs.parent_controller&.constantize || ActionController::Base
|
|
3
3
|
before_action :authenticate_onlylogs_user!
|
|
4
4
|
|
|
5
5
|
private
|
|
@@ -5,8 +5,8 @@ module Onlylogs
|
|
|
5
5
|
def index
|
|
6
6
|
@max_lines = (params[:max_lines] || 100).to_i
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
@log_file_path =
|
|
8
|
+
@available_log_files = Onlylogs.available_log_files
|
|
9
|
+
@log_file_path = selected_log_file_path
|
|
10
10
|
|
|
11
11
|
@filter = params[:filter]
|
|
12
12
|
@autoscroll = params[:autoscroll] != "false"
|
|
@@ -15,9 +15,24 @@ module Onlylogs
|
|
|
15
15
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
|
+
def selected_log_file_path
|
|
19
|
+
encrypted_path = params[:log_file_path]
|
|
20
|
+
return default_log_file_path if encrypted_path.blank?
|
|
21
|
+
|
|
22
|
+
decrypted_path = Onlylogs::SecureFilePath.decrypt(encrypted_path)
|
|
23
|
+
if Onlylogs.file_path_permitted?(decrypted_path)
|
|
24
|
+
decrypted_path
|
|
25
|
+
else
|
|
26
|
+
raise SecurityError, "File path not allowed"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
18
30
|
def default_log_file_path
|
|
19
31
|
# "/Users/alessandrorodi/RenuoWorkspace/onlylogs/test/fixtures/files/very_big.log"
|
|
20
|
-
Onlylogs.default_log_file_path
|
|
32
|
+
configured_default = Onlylogs.default_log_file_path
|
|
33
|
+
return configured_default if Onlylogs.file_path_permitted?(configured_default) && ::File.exist?(configured_default)
|
|
34
|
+
|
|
35
|
+
@available_log_files.first&.to_s || configured_default
|
|
21
36
|
end
|
|
22
37
|
end
|
|
23
38
|
end
|
|
@@ -17,25 +17,25 @@ module Onlylogs
|
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
19
19
|
# Pre-compiled regex for better performance
|
|
20
|
-
ANSI_REGEX = /\x1b\[(\d+)m
|
|
20
|
+
ANSI_REGEX = /\x1b\[(\d+)m/
|
|
21
21
|
|
|
22
22
|
# Pre-built HTML templates to avoid string interpolation (frozen for better performance)
|
|
23
23
|
HTML_TEMPLATES = {
|
|
24
|
-
"1" => '<span class="fw-bold">'
|
|
25
|
-
"30" => '<span class="log-black">'
|
|
26
|
-
"31" => '<span class="log-red">'
|
|
27
|
-
"32" => '<span class="log-green">'
|
|
28
|
-
"33" => '<span class="log-yellow">'
|
|
29
|
-
"34" => '<span class="log-blue">'
|
|
30
|
-
"35" => '<span class="log-magenta">'
|
|
31
|
-
"36" => '<span class="log-cyan">'
|
|
32
|
-
"37" => '<span class="log-white">'
|
|
33
|
-
"39" => '<span class="">'
|
|
34
|
-
"0" => ""
|
|
24
|
+
"1" => '<span class="fw-bold">',
|
|
25
|
+
"30" => '<span class="log-black">',
|
|
26
|
+
"31" => '<span class="log-red">',
|
|
27
|
+
"32" => '<span class="log-green">',
|
|
28
|
+
"33" => '<span class="log-yellow">',
|
|
29
|
+
"34" => '<span class="log-blue">',
|
|
30
|
+
"35" => '<span class="log-magenta">',
|
|
31
|
+
"36" => '<span class="log-cyan">',
|
|
32
|
+
"37" => '<span class="log-white">',
|
|
33
|
+
"39" => '<span class="">',
|
|
34
|
+
"0" => "" # Reset (no color)
|
|
35
35
|
}.freeze
|
|
36
36
|
|
|
37
37
|
# Pre-built closing span (frozen for better performance)
|
|
38
|
-
CLOSING_SPAN = "</span>"
|
|
38
|
+
CLOSING_SPAN = "</span>"
|
|
39
39
|
|
|
40
40
|
def self.parse(string)
|
|
41
41
|
return string if string.blank?
|
|
@@ -6,29 +6,29 @@ require "uri"
|
|
|
6
6
|
module Onlylogs
|
|
7
7
|
class FilePathParser
|
|
8
8
|
KNOWN_EDITORS = [
|
|
9
|
-
{
|
|
10
|
-
{
|
|
11
|
-
{
|
|
12
|
-
{
|
|
13
|
-
{
|
|
14
|
-
{
|
|
15
|
-
{
|
|
16
|
-
{
|
|
17
|
-
{
|
|
9
|
+
{symbols: [:atom], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}"},
|
|
10
|
+
{symbols: [:emacs, :emacsclient], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}"},
|
|
11
|
+
{symbols: [:idea], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}"},
|
|
12
|
+
{symbols: [:macvim, :mvim, :vim], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}"},
|
|
13
|
+
{symbols: [:rubymine, :mine], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}"},
|
|
14
|
+
{symbols: [:sublime, :subl, :st], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}"},
|
|
15
|
+
{symbols: [:textmate, :txmt, :tm, :mate], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}"},
|
|
16
|
+
{symbols: [:vscode, :code], sniff: /code/i, url: "vscode://file/%{file}:%{line}"},
|
|
17
|
+
{symbols: [:vscodium, :codium], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}"}
|
|
18
18
|
].freeze
|
|
19
19
|
|
|
20
20
|
# Pre-compiled regex for better performance
|
|
21
21
|
FILE_PATH_PATTERN = %r{
|
|
22
22
|
(?<![a-zA-Z0-9_/]) # Negative lookbehind - not preceded by word chars or /
|
|
23
23
|
(?:\./)? # Optional relative path indicator
|
|
24
|
-
(?:/[a-zA-Z0-9_
|
|
24
|
+
(?:/[a-zA-Z0-9_\-.\s]+)+ # File path with allowed characters
|
|
25
25
|
(?:\.rb|\.js|\.ts|\.tsx|\.jsx|\.py|\.java|\.go|\.rs|\.php|\.html|\.erb|\.haml|\.slim|\.css|\.scss|\.sass|\.less|\.xml|\.json|\.yml|\.yaml|\.md|\.txt|\.log) # File extensions
|
|
26
26
|
(?::\d+)? # Optional line number
|
|
27
27
|
(?![a-zA-Z0-9_/]) # Negative lookahead - not followed by word chars or /
|
|
28
|
-
}x
|
|
28
|
+
}x
|
|
29
29
|
|
|
30
30
|
# Pre-built HTML template to avoid string interpolation
|
|
31
|
-
HTML_TEMPLATE = '<a href="%{url}" class="file-link">%{match}</a>'
|
|
31
|
+
HTML_TEMPLATE = '<a href="%{url}" class="file-link">%{match}</a>'
|
|
32
32
|
|
|
33
33
|
def self.parse(string)
|
|
34
34
|
return string if string.blank?
|
|
@@ -40,7 +40,7 @@ module Onlylogs
|
|
|
40
40
|
file_path = extract_file_path(match)
|
|
41
41
|
line_number = extract_line_number(match)
|
|
42
42
|
url = cached_editor_instance.url(file_path, line_number)
|
|
43
|
-
HTML_TEMPLATE % {
|
|
43
|
+
HTML_TEMPLATE % {url: url, match: match}
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
|
|
@@ -73,7 +73,6 @@ module Onlylogs
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
|
|
77
76
|
def self.editor_from_symbol(symbol)
|
|
78
77
|
KNOWN_EDITORS.each do |preset|
|
|
79
78
|
return for_formatting_string(preset[:url]) if preset[:symbols].include?(symbol)
|
|
@@ -86,14 +85,14 @@ module Onlylogs
|
|
|
86
85
|
end
|
|
87
86
|
|
|
88
87
|
def url(raw_path, line)
|
|
89
|
-
if virtual_path && raw_path.start_with?(virtual_path)
|
|
88
|
+
file = if virtual_path && raw_path.start_with?(virtual_path)
|
|
90
89
|
if host_path
|
|
91
|
-
|
|
90
|
+
raw_path.sub(%r{\A#{virtual_path}}, host_path)
|
|
92
91
|
else
|
|
93
|
-
|
|
92
|
+
raw_path.sub(%r{\A#{virtual_path}/}, "")
|
|
94
93
|
end
|
|
95
94
|
else
|
|
96
|
-
|
|
95
|
+
raw_path
|
|
97
96
|
end
|
|
98
97
|
|
|
99
98
|
url_proc.call(file, line)
|
|
@@ -115,15 +114,19 @@ module Onlylogs
|
|
|
115
114
|
@host_path ||= ENV["ONLYLOGS_HOST_PATH"]
|
|
116
115
|
end
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
match.sub(/:\d+$/, "")
|
|
121
|
-
end
|
|
117
|
+
class << self
|
|
118
|
+
private
|
|
122
119
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
def extract_file_path(match)
|
|
121
|
+
# Remove line number if present
|
|
122
|
+
match.sub(/:\d+$/, "")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def extract_line_number(match)
|
|
126
|
+
# Extract line number or default to 1
|
|
127
|
+
line_match = match.match(/:(\d+)$/)
|
|
128
|
+
line_match ? line_match[1].to_i : 1
|
|
129
|
+
end
|
|
127
130
|
end
|
|
128
131
|
end
|
|
129
132
|
end
|
data/app/models/onlylogs/grep.rb
CHANGED
|
@@ -5,8 +5,8 @@ module Onlylogs
|
|
|
5
5
|
script_name = Onlylogs.ripgrep_enabled? ? "super_ripgrep" : "super_grep"
|
|
6
6
|
super_grep_path = ::File.expand_path("../../../bin/#{script_name}", __dir__)
|
|
7
7
|
|
|
8
|
-
command_args = [
|
|
9
|
-
command_args += [
|
|
8
|
+
command_args = [super_grep_path]
|
|
9
|
+
command_args += ["--max-matches", Onlylogs.max_line_matches.to_s] if Onlylogs.max_line_matches.present?
|
|
10
10
|
command_args << "--regexp" if regexp_mode
|
|
11
11
|
|
|
12
12
|
# Add byte range parameters if specified
|
|
@@ -15,7 +15,7 @@ module Onlylogs
|
|
|
15
15
|
command_args << "--end-position" << end_position.to_s if end_position
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
command_args += [
|
|
18
|
+
command_args += [pattern, file_path]
|
|
19
19
|
|
|
20
20
|
results = []
|
|
21
21
|
|
|
@@ -1 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
<style>
|
|
2
|
+
.grid {
|
|
3
|
+
display: grid;
|
|
4
|
+
grid-template-rows: 32px 1fr;
|
|
5
|
+
height: 100vh;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.grid-header {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
border-bottom: 1px solid lightgray;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.logo {
|
|
15
|
+
height: 20px;
|
|
16
|
+
margin: 3px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.log-file-dropdown {
|
|
20
|
+
margin-left: 0.5rem;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
<div class="grid">
|
|
24
|
+
<div class="grid-header">
|
|
25
|
+
<%= image_tag "onlylogs/logo.png", class: "logo" %>
|
|
26
|
+
<% if @available_log_files.any? %>
|
|
27
|
+
<%= form_with url: root_path, method: :get, local: true, class: "log-file-dropdown" do %>
|
|
28
|
+
<select id="log_file_path" name="log_file_path" onchange="this.form.submit()">
|
|
29
|
+
<% @available_log_files.each do |file| %>
|
|
30
|
+
<% encrypted_path = Onlylogs::SecureFilePath.encrypt(file.to_s) %>
|
|
31
|
+
<% relative_path = Pathname.new(file).relative_path_from(Rails.root).to_s %>
|
|
32
|
+
<option value="<%= encrypted_path %>" <%= "selected" if file.to_s == @log_file_path.to_s %>>
|
|
33
|
+
<%= relative_path %>
|
|
34
|
+
</option>
|
|
35
|
+
<% end %>
|
|
36
|
+
</select>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<%= render partial: "onlylogs/shared/log_container", locals: { log_file_path: @log_file_path, tail: @max_lines, filter: @filter, autoscroll: @autoscroll } %>
|
|
41
|
+
</div>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
mode = filter.blank? ? "live" : "search"
|
|
8
8
|
cursor_position = mode == "search" ? 0 : [File.size(log_file_path) - (tail * 100), 0].max
|
|
9
9
|
|
|
10
|
-
raise SecurityError, "File path not allowed" unless Onlylogs.
|
|
10
|
+
raise SecurityError, "File path not allowed" unless Onlylogs.file_path_permitted?(log_file_path)
|
|
11
11
|
|
|
12
12
|
encrypted_log_file_path = Onlylogs::SecureFilePath.encrypt(log_file_path)
|
|
13
13
|
%>
|
data/config/routes.rb
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Onlylogs
|
|
4
4
|
class Configuration
|
|
5
|
-
attr_accessor :
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
attr_accessor :log_file_patterns, :default_log_file_path, :basic_auth_user, :basic_auth_password,
|
|
6
|
+
:parent_controller, :disable_basic_authentication, :ripgrep_enabled, :editor,
|
|
7
|
+
:max_line_matches
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
|
-
@
|
|
10
|
+
@log_file_patterns = default_log_file_patterns
|
|
11
11
|
@default_log_file_path = default_log_file_path_value
|
|
12
12
|
@basic_auth_user = default_basic_auth_user
|
|
13
13
|
@basic_auth_password = default_basic_auth_password
|
|
@@ -44,7 +44,7 @@ module Onlylogs
|
|
|
44
44
|
:vscode
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def
|
|
47
|
+
def default_log_file_patterns
|
|
48
48
|
# Default to environment-specific log files (without rotation suffixes)
|
|
49
49
|
[
|
|
50
50
|
Rails.root.join("log/#{Rails.env}.log")
|
|
@@ -76,13 +76,33 @@ module Onlylogs
|
|
|
76
76
|
yield configuration
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
def self.
|
|
79
|
+
def self.file_path_permitted?(file_path)
|
|
80
80
|
path = ::File.expand_path(file_path.to_s)
|
|
81
81
|
|
|
82
|
-
configuration.
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
configuration.log_file_patterns.any? do |pattern|
|
|
83
|
+
allowed_file_patterns_for(pattern).any? do |pat|
|
|
84
|
+
::File.fnmatch?(pat, path, ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns all existing files on disk that match the configured allowed_files patterns.
|
|
90
|
+
# Supports direct file paths and glob patterns (e.g., *.log, **/*.log).
|
|
91
|
+
# Returns Pathname objects so callers can access both the basename and the absolute path.
|
|
92
|
+
def self.available_log_files
|
|
93
|
+
patterns = Array(configuration.log_file_patterns)
|
|
94
|
+
|
|
95
|
+
paths = patterns.flat_map do |pattern|
|
|
96
|
+
allowed_file_patterns_for(pattern).flat_map do |expanded_pattern|
|
|
97
|
+
if glob_pattern?(expanded_pattern)
|
|
98
|
+
Dir.glob(expanded_pattern, ::File::FNM_DOTMATCH | ::File::FNM_PATHNAME).select { |p| ::File.file?(p) }
|
|
99
|
+
else
|
|
100
|
+
::File.file?(expanded_pattern) ? [expanded_pattern] : []
|
|
101
|
+
end
|
|
102
|
+
end
|
|
85
103
|
end
|
|
104
|
+
|
|
105
|
+
paths.uniq.sort.map { |p| Pathname.new(p) }
|
|
86
106
|
end
|
|
87
107
|
|
|
88
108
|
def self.default_log_file_path
|
|
@@ -126,4 +146,23 @@ module Onlylogs
|
|
|
126
146
|
def self.max_line_matches
|
|
127
147
|
configuration.max_line_matches
|
|
128
148
|
end
|
|
149
|
+
|
|
150
|
+
def self.allowed_file_patterns_for(pattern)
|
|
151
|
+
absolute_pattern = ::File.expand_path(pattern.to_s)
|
|
152
|
+
if glob_pattern?(absolute_pattern)
|
|
153
|
+
return [absolute_pattern, "#{absolute_pattern}.*"] if absolute_pattern.end_with?(".log")
|
|
154
|
+
|
|
155
|
+
return [absolute_pattern]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
return [absolute_pattern, "#{absolute_pattern}.*"] if absolute_pattern.end_with?(".log")
|
|
159
|
+
|
|
160
|
+
[absolute_pattern]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def self.glob_pattern?(pattern)
|
|
164
|
+
pattern.match?(/[*?\[\]{}]/)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private_class_method :allowed_file_patterns_for, :glob_pattern?
|
|
129
168
|
end
|
data/lib/onlylogs/engine.rb
CHANGED
data/lib/onlylogs/formatter.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Onlylogs
|
|
|
12
12
|
def call(severity, time, progname, msg)
|
|
13
13
|
return nil if "Onlylogs::LogsChannel".in?(msg)
|
|
14
14
|
return nil if denylist.any? { |pattern| pattern.match?(msg) }
|
|
15
|
-
tags = [
|
|
15
|
+
tags = [time.iso8601, severity[0].upcase]
|
|
16
16
|
push_tags tags
|
|
17
17
|
str = super
|
|
18
18
|
pop_tags tags.size
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "socket"
|
|
4
|
-
require "thread"
|
|
5
4
|
|
|
6
5
|
# This logger sends messages to onlylogs.io via a UNIX socket connected to the onlylogs sidecar process.
|
|
7
6
|
# You need to have the onlylogs sidecar running for this to work.
|
|
@@ -29,7 +28,7 @@ module Onlylogs
|
|
|
29
28
|
|
|
30
29
|
formatted = format_message(format_severity(severity), Time.now, progname, message.to_s)
|
|
31
30
|
send_to_socket(formatted)
|
|
32
|
-
super
|
|
31
|
+
super
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
private
|
|
@@ -40,10 +39,10 @@ module Onlylogs
|
|
|
40
39
|
socket = ensure_socket
|
|
41
40
|
socket&.puts(payload)
|
|
42
41
|
rescue Errno::EPIPE, Errno::ECONNREFUSED, Errno::ENOENT => e
|
|
43
|
-
|
|
42
|
+
warn "Onlylogs::SocketLogger error: #{e.message}"
|
|
44
43
|
reconnect_socket
|
|
45
44
|
rescue => e
|
|
46
|
-
|
|
45
|
+
warn "Onlylogs::SocketLogger unexpected error: #{e.class}: #{e.message}"
|
|
47
46
|
reconnect_socket
|
|
48
47
|
end
|
|
49
48
|
|
|
@@ -53,7 +52,7 @@ module Onlylogs
|
|
|
53
52
|
@socket_mutex.synchronize do
|
|
54
53
|
@socket ||= UNIXSocket.new(@socket_path)
|
|
55
54
|
rescue => e
|
|
56
|
-
|
|
55
|
+
warn "Unable to connect to Onlylogs sidecar (#{@socket_path}): #{e.message}"
|
|
57
56
|
@socket = nil
|
|
58
57
|
end
|
|
59
58
|
|
|
@@ -62,7 +61,11 @@ module Onlylogs
|
|
|
62
61
|
|
|
63
62
|
def reconnect_socket
|
|
64
63
|
@socket_mutex.synchronize do
|
|
65
|
-
|
|
64
|
+
begin
|
|
65
|
+
@socket&.close
|
|
66
|
+
rescue
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
66
69
|
@socket = nil
|
|
67
70
|
end
|
|
68
71
|
end
|
data/lib/onlylogs/version.rb
CHANGED
|
@@ -26,12 +26,12 @@ Puma::Plugin.create do
|
|
|
26
26
|
FileUtils.mkdir_p(sockets_dir)
|
|
27
27
|
|
|
28
28
|
@socket_path = env_or_option("ONLYLOGS_SIDECAR_SOCKET", :onlylogs_socket,
|
|
29
|
-
|
|
29
|
+
File.join(sockets_dir, "onlylogs-sidecar.sock"))
|
|
30
30
|
@drain_url = ENV["ONLYLOGS_DRAIN_URL"] || @options[:onlylogs_drain_url]
|
|
31
31
|
@batch_size = env_or_option("ONLYLOGS_BATCH_SIZE", :onlylogs_batch_size, 100).to_i
|
|
32
32
|
@flush_interval = env_or_option("ONLYLOGS_FLUSH_INTERVAL", :onlylogs_flush_interval, 0.5).to_f
|
|
33
33
|
@sidecar_script = env_or_option("ONLYLOGS_SIDECAR_BIN", :onlylogs_sidecar_bin,
|
|
34
|
-
|
|
34
|
+
File.expand_path("../../../bin/onlylogs_sidecar", __dir__))
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def register_hooks
|
|
@@ -63,8 +63,8 @@ Puma::Plugin.create do
|
|
|
63
63
|
|
|
64
64
|
info "Starting Onlylogs sidecar (socket: #{@socket_path}, drain: #{@drain_url})"
|
|
65
65
|
@sidecar_pid = Process.spawn(env, RbConfig.ruby, @sidecar_script,
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
chdir: @app_root,
|
|
67
|
+
pgroup: true)
|
|
68
68
|
rescue Errno::ENOENT => e
|
|
69
69
|
error "Unable to start sidecar: #{e.message}"
|
|
70
70
|
end
|
|
@@ -85,7 +85,11 @@ Puma::Plugin.create do
|
|
|
85
85
|
# Already stopped
|
|
86
86
|
rescue Timeout::Error
|
|
87
87
|
warn "Sidecar did not stop in time, killing"
|
|
88
|
-
|
|
88
|
+
begin
|
|
89
|
+
Process.kill("KILL", -pgid)
|
|
90
|
+
rescue
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
89
93
|
ensure
|
|
90
94
|
@sidecar_pid = nil
|
|
91
95
|
remove_socket_file
|