Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
# options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- name: Install packages
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config google-chrome-stable
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config google-chrome-stable ripgrep

- name: Checkout code
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion app/models/onlylogs/ansi_color_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AnsiColorParser
}.freeze

# Pre-built closing span (frozen for better performance)
CLOSING_SPAN = '</span>'.freeze
CLOSING_SPAN = "</span>".freeze

def self.parse(string)
return string if string.blank?
Expand Down
4 changes: 2 additions & 2 deletions app/models/onlylogs/batch_sender.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,13 @@ def add_line(line_data)

def send_batch
lines_to_send = nil

@mutex.synchronize do
return if @buffer.empty?
lines_to_send = @buffer.dup
@buffer.clear
end

return if lines_to_send.empty?

@channel.send(:transmit, {
Expand Down
25 changes: 18 additions & 7 deletions app/models/onlylogs/file_path_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ class FilePathParser
{ symbols: [ :atom ], sniff: /atom/i, url: "atom://core/open/file?filename=%{file}&line=%{line}" },
{ symbols: [ :emacs, :emacsclient ], sniff: /emacs/i, url: "emacs://open?url=file://%{file}&line=%{line}" },
{ symbols: [ :idea ], sniff: /idea/i, url: "idea://open?file=%{file}&line=%{line}" },
{ symbols: [ :macvim, :mvim ], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
{ symbols: [ :rubymine ], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
{ symbols: [ :macvim, :mvim, :vim ], sniff: /vim/i, url: "mvim://open?url=file://%{file_unencoded}&line=%{line}" },
{ symbols: [ :rubymine, :mine ], sniff: /mine/i, url: "x-mine://open?file=%{file}&line=%{line}" },
{ symbols: [ :sublime, :subl, :st ], sniff: /subl/i, url: "subl://open?url=file://%{file}&line=%{line}" },
{ symbols: [ :textmate, :txmt, :tm ], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
{ symbols: [ :textmate, :txmt, :tm, :mate ], sniff: /mate/i, url: "txmt://open?url=file://%{file}&line=%{line}" },
{ symbols: [ :vscode, :code ], sniff: /code/i, url: "vscode://file/%{file}:%{line}" },
{ symbols: [ :vscodium, :codium ], sniff: /codium/i, url: "vscodium://file/%{file}:%{line}" }
].freeze
Expand Down Expand Up @@ -46,7 +46,11 @@ def self.parse(string)

def self.for_formatting_string(formatting_string)
new proc { |file, line|
formatting_string % { file: URI.encode_www_form_component(file), file_unencoded: file, line: line }
formatting_string % {
file: URI.encode_www_form_component(file),
file_unencoded: file,
line: line
}
}
end

Expand All @@ -57,11 +61,18 @@ def self.for_proc(url_proc)
# Cache for the editor instance
@cached_editor_instance = nil

def self.clear_editor_cache
@cached_editor_instance = nil
end

def self.cached_editor_instance
return @cached_editor_instance if @cached_editor_instance
@cached_editor_instance = editor_from_symbol(Onlylogs.editor)
@cached_editor_instance ||= if ENV["ONLYLOGS_EDITOR_URL"]
for_formatting_string(ENV["ONLYLOGS_EDITOR_URL"])
else
editor_from_symbol(Onlylogs.editor)
end
end


def self.editor_from_symbol(symbol)
KNOWN_EDITORS.each do |preset|
Expand Down
1 change: 0 additions & 1 deletion config/importmap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from Onlylogs::Engine.root.join("app/javascript/onlylogs/controllers"), under: "controllers", to: "onlylogs/controllers"

12 changes: 6 additions & 6 deletions lib/onlylogs/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def initialize
@parent_controller = nil
@disable_basic_authentication = false
@ripgrep_enabled = default_ripgrep_enabled
@editor = default_editor
@editor = nil
@max_line_matches = 100000
end

Expand All @@ -26,20 +26,20 @@ def default_editor
if (credentials_editor = Rails.application.credentials.dig(:onlylogs, :editor))
return credentials_editor
end

# 2. Check environment variables (ONLYLOGS_EDITOR > RAILS_EDITOR > EDITOR)
if ENV["ONLYLOGS_EDITOR"]
return ENV["ONLYLOGS_EDITOR"].to_sym
end

if ENV["RAILS_EDITOR"]
return ENV["RAILS_EDITOR"].to_sym
end

if ENV["EDITOR"]
return ENV["EDITOR"].to_sym
end

# 3. Default fallback
:vscode
end
Expand Down Expand Up @@ -114,7 +114,7 @@ def self.ripgrep_enabled?
end

def self.editor
configuration.default_editor
configuration.editor || configuration.default_editor
end

def self.editor=(editor_symbol)
Expand Down
3 changes: 1 addition & 2 deletions lib/onlylogs/socket_logger.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def initialize(local_fallback: $stdout, socket_path: ENV.fetch("ONLYLOGS_SIDECAR
end

def add(severity, message = nil, progname = nil, &block)

if message.nil?
if block_given?
message = block.call
Expand All @@ -44,7 +43,7 @@ def send_to_socket(payload)
puts "Onlylogs::SocketLogger error: #{e.message}"
reconnect_socket
rescue => e
puts"Onlylogs::SocketLogger unexpected error: #{e.class}: #{e.message}"
puts "Onlylogs::SocketLogger unexpected error: #{e.class}: #{e.message}"
reconnect_socket
end

Expand Down
2 changes: 1 addition & 1 deletion onlylogs.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md", "bin/onlylogs_sidecar"]
end
spec.bindir = "bin"
spec.executables = ["onlylogs_sidecar"]
spec.executables = [ "onlylogs_sidecar" ]

spec.add_dependency "rails", "~> 8.0"
end
1 change: 0 additions & 1 deletion test/dummy/app/models/book.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
class Book < ApplicationRecord

end
17 changes: 16 additions & 1 deletion test/models/onlylogs/file_path_parser_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,26 @@ class Onlylogs::FilePathParserTest < ActiveSupport::TestCase
def setup
@original_editor = ENV["EDITOR"]
@original_onlylogs_editor = ENV["ONLYLOGS_EDITOR"]
@original_onlylogs_url = ENV["ONLYLOGS_EDITOR_URL"]
@original_virtual_path = ENV["ONLYLOGS_VIRTUAL_PATH"]
@original_host_path = ENV["ONLYLOGS_HOST_PATH"]

ENV["ONLYLOGS_EDITOR_URL"] = nil
ENV["ONLYLOGS_VIRTUAL_PATH"] = nil
ENV["ONLYLOGS_HOST_PATH"] = nil
Onlylogs.configuration.editor = nil
Onlylogs::FilePathParser.clear_editor_cache
end

def teardown
ENV["EDITOR"] = @original_editor
ENV["ONLYLOGS_EDITOR"] = @original_onlylogs_editor
ENV["ONLYLOGS_EDITOR_URL"] = @original_onlylogs_url
ENV["ONLYLOGS_VIRTUAL_PATH"] = @original_virtual_path
ENV["ONLYLOGS_HOST_PATH"] = @original_host_path

Onlylogs.configuration.editor = nil
Onlylogs::FilePathParser.clear_editor_cache
end

test "converts file path with line number to clickable link" do
Expand Down Expand Up @@ -119,7 +134,7 @@ def teardown
ENV["ONLYLOGS_EDITOR_URL"] = nil
input = "Error in /path/to/file.rb:42"
result = Onlylogs::FilePathParser.parse(input)
assert_includes result, 'href="vscode://open?url=file://%2Fpath%2Fto%2Ffile.rb&line=42"'
assert_includes result, 'href="vscode://file/%2Fpath%2Fto%2Ffile.rb:42"'
end

test "handles virtual path mapping" do
Expand Down
10 changes: 5 additions & 5 deletions test/models/onlylogs/file_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def setup
test_file.go_to_position(position_after_line_3)
assert_equal position_after_line_3, test_file.last_position
result = test_file.send(:read_new_lines)
expected = ["Line 4", "Line 5", "Line 6", "Line 7", "Line 8", "Line 9", "Line 10"]
expected = [ "Line 4", "Line 5", "Line 6", "Line 7", "Line 8", "Line 9", "Line 10" ]
assert_equal expected, result
ensure
File.delete(test_file_path) if File.exist?(test_file_path)
Expand All @@ -49,7 +49,7 @@ def setup
test_file = Onlylogs::File.new(test_file_path, last_position: 0)
result = test_file.send(:read_new_lines)

expected = ["Line 0", "Line 1", "Line 2"]
expected = [ "Line 0", "Line 1", "Line 2" ]
assert_equal expected, result
assert_equal 21, test_file.last_position # "Line 0\nLine 1\nLine 2\n".bytesize

Expand All @@ -61,7 +61,7 @@ def setup
end

result = test_file.send(:read_new_lines)
expected = ["Line 3", "Line 4", "Line 5", "Line 6"]
expected = [ "Line 3", "Line 4", "Line 5", "Line 6" ]
assert_equal expected, result

File.open(test_file_path, "a") do |f|
Expand All @@ -71,7 +71,7 @@ def setup
end

result = test_file.send(:read_new_lines)
expected = ["Line 7", "Line 8"]
expected = [ "Line 7", "Line 8" ]
assert_equal expected, result

File.open(test_file_path, "a") do |f|
Expand All @@ -82,7 +82,7 @@ def setup

# Should return the completed line plus the 2 new lines
result = test_file.send(:read_new_lines)
expected = ["Incomplete Line 9", "Line 10", "Line 11"]
expected = [ "Incomplete Line 9", "Line 10", "Line 11" ]
assert_equal expected, result
ensure
File.delete(test_file_path) if File.exist?(test_file_path)
Expand Down
9 changes: 7 additions & 2 deletions test/models/onlylogs/grep_performance_test.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# This test is a performance test and can be run locally with the use of the downloaded big file but will not be run on the CI
require "test_helper"


class Onlylogs::GrepPerformanceTest < ActiveSupport::TestCase
def setup
@fixture_path = ::File.expand_path("../../fixtures/files/log_file_100_lines.txt", __dir__)
@large_fixture_path = ::File.expand_path("../../fixtures/files/big.log", __dir__)

skip "Performance test: not run on CI" if ENV["CI"]
skip "Performance test: big.log not found at #{@large_fixture_path}" unless ::File.exist?(@large_fixture_path)
end

test "it does not allocate memory for results when using a block" do
# Use the same large test file for fair comparison
large_fixture_path = ::File.expand_path("../../fixtures/files/big.log", __dir__)
large_fixture_path = @large_fixture_path

# Force garbage collection to get a clean baseline
GC.start
Expand Down Expand Up @@ -61,7 +66,7 @@ def setup

test "it allocates memory for results when not using a block" do
# Use a larger test file to make memory differences more visible
large_fixture_path = ::File.expand_path("../../fixtures/files/big.log", __dir__)
large_fixture_path = @large_fixture_path

# Force garbage collection to get a clean baseline
GC.start
Expand Down
40 changes: 22 additions & 18 deletions test/models/onlylogs/grep_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ def setup
@fixture_path = ::File.expand_path("../../fixtures/files/log_file_100_lines.txt", __dir__)
@special_lines_path = File.expand_path("../../fixtures/files/log_special_lines.txt", __dir__)
@original_ripgrep_enabled = Onlylogs.ripgrep_enabled?
@original_max_line_matches = Onlylogs.max_line_matches

Onlylogs.configuration.max_line_matches = nil
end

def teardown
Onlylogs.configuration.ripgrep_enabled = @original_ripgrep_enabled
Onlylogs.configuration.max_line_matches = @original_max_line_matches
end

def self.test_both_engine_modes(test_name, &block)
test test_name do
[false, true].each do |ripgrep_enabled|
[ false, true ].each do |ripgrep_enabled|
Onlylogs.configuration.ripgrep_enabled = ripgrep_enabled
engine_name = ripgrep_enabled ? 'ripgrep' : 'grep'
engine_name = ripgrep_enabled ? "ripgrep" : "grep"
instance_exec(engine_name, &block)
end
end
Expand Down Expand Up @@ -78,11 +82,11 @@ def self.test_both_engine_modes(test_name, &block)
# In literal mode, dot should match literal dot
lines_literal = Onlylogs::Grep.grep("(0.0ms)", @special_lines_path, regexp_mode: false)
assert_equal 1, lines_literal.length, "Failed with #{engine_name}"

# In regexp mode, dot should match any character
lines_regexp = Onlylogs::Grep.grep("(0\\.0ms)", @special_lines_path, regexp_mode: true)
assert_equal 1, lines_regexp.length, "Failed with #{engine_name}"

# Test that regexp mode with dot wildcard matches more broadly
lines_wildcard = Onlylogs::Grep.grep("(0.0ms)", @special_lines_path, regexp_mode: true)
assert_equal 1, lines_wildcard.length, "Failed with #{engine_name}"
Expand All @@ -92,7 +96,7 @@ def self.test_both_engine_modes(test_name, &block)
# Test character class [A-Z] to match uppercase letters
lines = Onlylogs::Grep.grep("\\[INFO\\]", @fixture_path, regexp_mode: true)
assert_equal 50, lines.length, "Failed with #{engine_name}"

# Test that literal mode treats brackets as literal characters
lines_literal = Onlylogs::Grep.grep("[INFO]", @fixture_path, regexp_mode: false)
assert_equal 50, lines_literal.length, "Failed with #{engine_name}"
Expand All @@ -102,40 +106,40 @@ def self.test_both_engine_modes(test_name, &block)
# Test + quantifier to match one or more digits
lines = Onlylogs::Grep.grep("Line \\d+", @fixture_path, regexp_mode: true)
assert_equal 100, lines.length, "Failed with #{engine_name}"

# Test that literal mode treats + as literal character
lines_literal = Onlylogs::Grep.grep("Line +", @fixture_path, regexp_mode: false)
assert_equal 0, lines_literal.length, "Failed with #{engine_name}"
end

test "match_line? supports regexp mode with dot wildcard" do
line = "ActiveRecord::SchemaMigration Load (0.0ms) SELECT ..."

# In literal mode, dot should match literal dot
assert Onlylogs::Grep.match_line?(line, "(0.0ms)", regexp_mode: false)

# In regexp mode, escaped dot should match literal dot
assert Onlylogs::Grep.match_line?(line, "\\(0\\.0ms\\)", regexp_mode: true)

# In regexp mode, unescaped dot should match any character
assert Onlylogs::Grep.match_line?(line, "\\(0.0ms\\)", regexp_mode: true)

# Test that literal mode treats dot as literal
refute Onlylogs::Grep.match_line?(line, "(0X0ms)", regexp_mode: false)
end

test "match_line? supports regexp mode with character classes" do
line = "[INFO] Application started - Line 1"

# Test escaped brackets to match literal brackets
assert Onlylogs::Grep.match_line?(line, "\\[INFO\\]", regexp_mode: true)

# Test character class [A-Z] to match uppercase letters (should not match [INFO])
refute Onlylogs::Grep.match_line?(line, "\\[A-Z\\]INFO", regexp_mode: true)

# Test that literal mode treats brackets as literal characters
refute Onlylogs::Grep.match_line?(line, "[A-Z]INFO", regexp_mode: false)

# Test a line that would match a simple regexp pattern
line_with_numbers = "Error 404: Page not found"
assert Onlylogs::Grep.match_line?(line_with_numbers, "Error \\d+:", regexp_mode: true)
Expand All @@ -145,11 +149,11 @@ def self.test_both_engine_modes(test_name, &block)
# Set a very low max_line_matches to test limiting
original_max_matches = Onlylogs.max_line_matches
Onlylogs.configuration.max_line_matches = 5

# This should return only 5 results even though there are more matches
lines = Onlylogs::Grep.grep("Line", @fixture_path)
assert_equal 5, lines.length, "Failed with #{engine_name}"

# Restore original configuration
Onlylogs.configuration.max_line_matches = original_max_matches
end
Expand All @@ -158,11 +162,11 @@ def self.test_both_engine_modes(test_name, &block)
# Set max_line_matches to nil to test no limits
original_max_matches = Onlylogs.max_line_matches
Onlylogs.configuration.max_line_matches = nil

# This should return all matches (100 lines in the fixture file)
lines = Onlylogs::Grep.grep("Line", @fixture_path)
assert_equal 100, lines.length, "Failed with #{engine_name}"

# Restore original configuration
Onlylogs.configuration.max_line_matches = original_max_matches
end
Expand Down
Loading