Stream Rails Logs to the Google Cloud
Part 1: Replacing Rails Logger with Lograge
The stackdriver
gem contains a bunch of monitoring tools:
google-cloud-debugger
google-cloud-error_reporting
google-cloud-logging
google-cloud-trace
Only google-cloud-logging
is needed for logs though.
Configuring GCP
Navigate to the Google Cloud Console and create an IAM & admin → Service Account with a Logging Admin rights. Then navigate to IAM & admin → IAM and make sure the service account has a role Logging Admin, click Add otherwise.
How to stream Rails logs to the GCP
Binding a default Rails logger to the GCP is quite easy, add the following code to the config/application.rb
:
require "google/cloud/logging"
credentials = Google::Cloud::Logging::Credentials.new(JSON.parse(*service_account_key*))
config.google_cloud.project_id = *project_id*
config.google_cloud.keyfile = credentials # or *path_to_keyfile*
config.google_cloud.use_logging = true # if you want to send non-production logs
The google-cloud-logging
gem is well-documented, you can find the examples for how to:
One of the reasons logs don't appear in the Google Cloud is incorrect rights, to check what went wrong you can debug the logger writer:
$ bundle exec rails c
> Rails.logger # Must be a Google Cloud instance
> Rails.logger.info "test" # Try to create a log
...
> Rails.logger.writer.last_exception # See what went wrong
How to stream Lograge logs to the GCP
The set of existing formatters for this gem is located here: lib/lograge/formatters. We'd like to stream jsonPayload
to the cloud, however if you select Lograge::Formatters::Json.new
as a formatter — lograge will send a textPayload
. That's because this formatter does JSON.dump(message)
which transforms a Hash to a String with JSON inside(which is good if you want to send it to the STDOUT
) and that String is being sent to the cloud. To have a jsonPayload
in the cloud you need to send a Hash without any transformations. So let's write our own formatter, lib/lograge/formatters/json_custom.rb
:
# frozen_string_literal: true
module Lograge
module Formatters
class JsonCustom
# @param data [Hash] Contains the log message as key-values.
# In development-like environments we'd like to convert
# this hash to a JSON string to display in STDOUT/file/...
# In production-like environments we'd like to send it raw
# to the Google Cloud, which interprets it as JSON payload.
def call(data)
if Log::GCLOUD_ENVIRONMENTS.include?(Rails.env.to_sym)
data
else
JSON.dump(data)
end
end
end
end
end
And configure the lograge custom events class to send JSON to STDOUT
and Hash to the GCP:
# frozen_string_literal: true
# Adds logger Log.*level* methods
module Log
extend self
GCLOUD_ENVIRONMENTS = %i[production staging].freeze
%i[debug info warn error fatal unknown].each do |severity|
define_method severity do |message, params = {}|
raise ArgumentError, "Hash is expected as 'params'" unless params.is_a?(Hash)
payload = {
m: message
}.merge(params)
unless GCLOUD_ENVIRONMENTS.include?(Rails.env.to_sym)
payload = payload.to_json
end
logger.public_send(severity, payload)
end
end
private
def logger
Rails.application.config.lograge.logger
end
end
Setting up Google Cloud auth and lograge
The config/initializers/lograge.rb
will do the GCP authentication for Log::GCLOUD_ENVIRONMENTS
, otherwise set STDOUT
as a logging stream. :credentials
is a GCP JSON key.
# frozen_string_literal: true
if Log::GCLOUD_ENVIRONMENTS.include?(Rails.env.to_sym)
require "google/cloud/logging"
google_cloud_config = Rails.application.credentials.dig(:google_cloud, Rails.env.to_sym)
credentials = Google::Cloud::Logging::Credentials.new(
JSON.parse(google_cloud_config[:credentials])
)
logging = Google::Cloud::Logging.new(
project_id: google_cloud_config[:project_id],
credentials: credentials
)
resource = logging.resource("gae_app", module_id: "1")
logger = logging.logger(Rails.env, resource, env: Rails.env.to_sym)
else
logger = ActiveSupport::Logger.new(STDOUT)
end
Rails.application.configure do
config.lograge.enabled = true
config.lograge.keep_original_rails_log = true
config.lograge.formatter = Lograge::Formatters::JsonCustom.new
config.lograge.logger = logger
config.lograge.ignore_actions = [
"ActiveStorage::DiskController#show",
"ActiveStorage::BlobsController#show",
"ActiveStorage::RepresentationsController#show"
]
config.lograge.custom_options = lambda do |event|
{
user_id: event.payload[:user_id]
}
end
end
To add user_id
to the payload add the following method to the application_controller.rb
:
def append_info_to_payload(payload)
super
payload[:user_id] = current_user&.id
end