Live stream log Rails tới trình duyệt
Bài đăng này đã không được cập nhật trong 4 năm
Tản mạn
MÌnh khá là bị thu hút bời các file Log =))
Logger là rất quan trọng trong quá trình development và operation một ứng dụng.
Như tiêu đề bài viết, hôm nay mình sẽ thử stream log tới browser.
Để dễ tưởng tượng những gì chúng ta sắp làm. Bạn có thể mở file log develoment rails bằng lệnh
tail -f log/development.log
Những thay đổi trong file log sẽ được hiển thị realtime ở đây.
Kết quả mong muốn cuối cùng của mình sẽ là tương tự chức năng stream log của datadog. Nhưng phần tách các loại log khó quá nên để sau vầy :3

Thực hiện
Khởi tạo rails app
rails new file-streaming-app
Tạo LiveFileStreamsController
touch live_file_streams_controller.rb
class LiveFileStreamsController < ApplicationController
end
Thêm router
resources :live_streams, only: [] do
  collection do
   get :log_file
  end
end
Phát trực tiếp với “response.stream”
Chúng ta sẽ sử dụng response.stream của ActionController::Streaming
class LiveStreamsController < ApplicationController
  def log_file
    5.times {
      response.stream.write "hello world\n"
      sleep 0.2
    }
    response.stream.close
  end
end
Bạn có thể xem thêm về ActionController::Streaming tại dây
Xem response ở trình duyệt
rails s
Vào:  localhost:3000/live_streams/log_file
Chúng ta sẽ thấy “hello world” được in 5 lần trong trình duyệt Response được in ra màn hình cùng một lúc, ngay cả khi đã đặt sleep ở giữa các lần stream.write

Mình sẽ cần xử lí để các nội dung này in ra 1 cách lần lượt
Include ActionController::Live
class LiveStreamsController < ApplicationController
  include ActionController::Live
  def log_file
    response.headers['Content-Type'] = 'text/event-stream'
    response.headers['Last-Modified'] = Time.now.httpdate
  
    5.times {
      response.stream.write "hello world\n"
      sleep 0.2
    }
    response.stream.close
  end
end
Bây h chúng ta sẽ thấy nội dung được in lần lượt ra màn hình

Như vậy ta stream được các nội dung thực hiện trong logic. Vậy bây giờ sẽ là tìm cách để stream các thay đổi trong file log
Server-side events
Nếu chưa bao giờ nghe nói về Server-side events (SSE), có thể đọc thêm về nó ở đây https://www.html5rocks.com/en/tutorials/eventsource/basics/
Về cơ bản, trình duyệt giữ kết nối mở cho máy chủ và kích hoạt một sự kiện trong JavaScript mỗi khi máy chủ gửi dữ liệu.
Tạo “file_streaming_app / sse.rb”
touch lib/file_streaming_app/sse.rb
require 'json'
module FileStreamingApp
  class SSE
    def initialize(io)
      @io = io
    end
    def write(object)
      @io.write "#{JSON.dump(object)}"
    end
    def close
      @io.close
    end
  end
end
Sử dụng “SSE” bên trong controller
require 'file_streaming_app/sse'
class LiveStreamsController < ApplicationController
  def log_file
    response.headers['Content-Type'] = 'text/event-stream'
    response.headers['Last-Modified'] = Time.now.httpdate
    sse = FileStreamingApp::SSE.new(response.stream)
    5.times {
      sse.write('hello world')
      sleep 0.5
    }
  ensure
    sse.close
  end
end
Sử dụng “filewatcher” để xem các thay đổi trong file
Để biết khi nào file được thay đổi, có 1 Gem có sẵn khá là tiện dụng đó là filewatcher
gem 'filewatcher', '~> 1.1.1'
Install gem bằng
bundle install
Đọc file log
touch lib/file_streaming_app/log_file.rb
module FileStreamingApp
  class LogFile
    def added_lines(file_path)
      file_content = File.open(file_path).readlines
      file_content.last(20)
    end
  end
end
Stream file log khi nó được sửa đổi
Update lại code Controller
def log_file
    response.headers['Content-Type'] = 'text/event-stream'
    response.headers['Last-Modified'] = Time.now.httpdate
    sse = FileStreamingApp::SSE.new(response.stream)
    log_file_path = Rails.root.join('log/development.log').to_s
    file = FileStreamingApp::LogFile.new
    # watch development.log file for changes
    Filewatcher.new([log_file_path]).watch do |_file_path, event_type|
      next unless event_type.to_s.eql?('updated')
      file_lines = file.added_lines(log_file_path)
      sse.write(file_lines)
    end
  ensure
    sse.close
  end
Sửa lại "SSE"
def write(file_lines)
  file_lines.each do |line|
    @io.write line
  end
end
Check thành quả thôi

Xử lí đa luồng
Mặc định, ở môi trường develop rails, các request được xử lí trong 1 luồng. Nên các request sẽ phải chờ request trước đó được thực thi xong.
Để giải quyết vấn đề đó, đi ăn trộm từ Stack Overflow .
vim config/environments/development.rb
Rails.application.configure do
  # other configurations
  config.middleware.delete Rack::Lock
end
Chỉ Stream các dòng đã thay đổi trong file log
Chúng ta sẽ cần nhớ vị trí của dòng cuối cùng trong file log trước khi thay đổi và chỉ hiển thị các dòng sau vị trí đó.
Sửa lại code 1 chút
class LogFile
  def added_lines(file_path)
    file_content = File.open(file_path).readlines
    total_lines = file_content.length
    @last_known_line_position ||= initial_line_position(total_lines)
    start_position = @last_known_line_position
    @last_known_line_position = total_lines
    file_content[start_position, total_lines]
  end
  private
  def initial_line_position(total_lines)
    return 0 if total_lines.zero? || total_lines <= 20
    # print last 20 lines from the file if event is emitted for the first time
    total_lines - 20
  end
end
Everithing is OK :V

All rights reserved
 
  
 