Skip to content

Instantly share code, notes, and snippets.

@amkisko
Last active March 12, 2025 10:37
Show Gist options
  • Save amkisko/84319ab911730908f9b570c4e04aaa0d to your computer and use it in GitHub Desktop.
Save amkisko/84319ab911730908f9b570c4e04aaa0d to your computer and use it in GitHub Desktop.
Customizable logger for grape ruby
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
class API < Grape::API
...
use GrapeLogger::GrapeInstrumentation
...
end
# 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