Last active
March 12, 2025 10:37
-
-
Save amkisko/84319ab911730908f9b570c4e04aaa0d to your computer and use it in GitHub Desktop.
Customizable logger for grape ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "rails_helper" | |
require "stringio" | |
require "json" | |
RSpec.describe API do | |
include Rack::Test::Methods | |
def app | |
API | |
end | |
describe "request logging" do | |
let(:log_output) { StringIO.new } | |
let(:password) { "secret-password-1234567890" } | |
let(:user) { create(:user, username: "test.user", password: password) } | |
let(:sensitive_params) do | |
{ | |
data: { | |
attributes: { | |
username: user.username, | |
password: password | |
} | |
} | |
} | |
end | |
before do | |
@original_logger = Rails.application.config.logger | |
@original_log_level = Rails.application.config.log_level | |
@original_lograge_enabled = Rails.application.config.lograge.enabled | |
@original_lograge_logger = Rails.application.config.lograge.logger | |
@original_lograge_formatter = Rails.application.config.lograge.formatter | |
test_logger = Logger.new(log_output) | |
test_logger.formatter = proc do |severity, datetime, progname, msg| | |
"#{msg}\n" | |
end | |
# Configure Rails logger | |
Rails.logger = test_logger | |
Rails.logger.level = :debug | |
Rails.application.config.logger = test_logger | |
Rails.application.config.log_level = :debug | |
Rails.application.config.lograge.enabled = true | |
Rails.application.config.lograge.logger = test_logger | |
Rails.application.config.lograge.formatter = Lograge::Formatters::Json.new | |
# Add debug logging | |
test_logger.info("Test starting - logger configured") | |
end | |
after do | |
Rails.application.config.logger = @original_logger | |
Rails.application.config.log_level = @original_log_level | |
Rails.application.config.lograge.enabled = @original_lograge_enabled | |
Rails.application.config.lograge.logger = @original_lograge_logger | |
Rails.application.config.lograge.formatter = @original_lograge_formatter | |
end | |
def parse_json_logs | |
log_output.string.split("\n").map do |line| | |
next unless line.start_with?('{') | |
JSON.parse(line) | |
end.compact | |
end | |
it "filters sensitive parameters in logs with Lograge format" do | |
# Make a test request that includes sensitive data | |
post "/api/sessions", sensitive_params | |
# Parse JSON logs | |
json_logs = parse_json_logs | |
request_log = json_logs.find { |log| log['method'] == 'POST' } | |
# Verify request logging | |
expect(request_log).to be_present | |
expect(request_log['method']).to eq('POST') | |
expect(request_log['path']).to eq('/api/sessions') | |
expect(request_log['controller']).to eq('API') | |
expect(request_log['action']).to be_present | |
# Verify parameter filtering | |
params = request_log['params'] | |
expect(params).to be_present | |
expect(params.to_s).not_to include(password) | |
expect(params.to_s).to include('[FILTERED]') | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class API < Grape::API | |
... | |
use GrapeLogger::GrapeInstrumentation | |
... | |
end | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# PATH: config/initializer/grape_logger.rb | |
module GrapeLogger | |
class GrapeInstrumentation < ::Grape::Middleware::Base | |
def call!(env) | |
Rails.logger.debug "GrapeInstrumentation: Processing request" | |
ActiveSupport::Notifications.instrument('grape.request', env: env) do |payload| | |
Rails.logger.debug "GrapeInstrumentation: Inside instrumentation block" | |
response = @app.call(env) | |
# Add response status to payload | |
payload[:status] = response[0] | |
payload[:response] = response | |
response | |
end | |
end | |
end | |
class GrapeRequestLogSubscriber < ActiveSupport::LogSubscriber | |
FILTERED_PARAMS = %w[password secret token key].freeze | |
def grape_request(event) | |
Rails.logger.debug "GrapeRequestLogSubscriber: Received grape.request event" | |
env = event.payload[:env] | |
data = extract_request(env) | |
data.merge!( | |
duration: event.duration.round(2), | |
status: event.payload[:status] | |
) | |
if defined?(Lograge) && Rails.application.config.lograge.enabled | |
Rails.logger.info format_lograge_message(data) | |
else | |
Rails.logger.info format_message(data) | |
end | |
rescue => e | |
Rails.logger.error "Error in GrapeRequestLogSubscriber: #{e.message}" | |
Rails.logger.error e.backtrace.join("\n") | |
end | |
private | |
def extract_request(env) | |
request = ::Grape::Request.new(env) | |
params = request.params.respond_to?(:to_unsafe_h) ? request.params.to_unsafe_h : request.params.to_h | |
{ | |
method: request.request_method, | |
path: request.path, | |
params: filter_params(params.except('route_info')), | |
remote_addr: request.ip, | |
user_agent: request.user_agent, | |
controller: 'API', | |
action: extract_action(env) | |
} | |
end | |
def extract_action(env) | |
endpoint = env['api.endpoint'] | |
return 'unknown' unless endpoint | |
# Try to get the action name from the endpoint | |
if endpoint.options[:path] && endpoint.options[:method] | |
"#{endpoint.options[:method].first}_#{endpoint.options[:path].first}" | |
else | |
'unknown' | |
end | |
end | |
def filter_params(params) | |
return {} if params.nil? | |
params.deep_transform_values do |value| | |
if value.is_a?(String) && FILTERED_PARAMS.any? { |param| value.to_s.include?(param) } | |
"[FILTERED]" | |
else | |
value | |
end | |
end | |
end | |
def format_message(data) | |
formatted_data = data.map { |k, v| "#{k.inspect}=>#{v.inspect}" }.join(", ") | |
"{#{formatted_data}}" | |
end | |
def format_lograge_message(data) | |
# Convert data to match Lograge's expected format | |
lograge_data = { | |
method: data[:method], | |
path: data[:path], | |
format: 'json', | |
controller: data[:controller], | |
action: data[:action], | |
status: data[:status], | |
duration: data[:duration], | |
params: data[:params], | |
remote_ip: data[:remote_addr], | |
user_agent: data[:user_agent], | |
timestamp: Time.current, | |
time: Time.current.utc.iso8601(3) | |
} | |
# Use Lograge's formatter | |
formatter = Rails.application.config.lograge.formatter | |
formatter.call(lograge_data) | |
end | |
def logger | |
Rails.logger | |
end | |
end | |
end | |
# Subscribe to grape request notifications | |
ActiveSupport::Notifications.subscribe('grape.request') do |*args| | |
event = ActiveSupport::Notifications::Event.new(*args) | |
GrapeLogger::GrapeRequestLogSubscriber.new.grape_request(event) | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment