L2meter is a little gem for building logfmt-compatiable loggers.
A new logger might be created like so:
logger = L2meter.buildConsider making the logger a constant to make it easier to use across different components of the app or globally.
The base log method accepts two type of arguments: bare values and key-value
pairs in form of hashes.
logger.log "Hello world" # => hello-world
logger.log :db_query, result: :success # => db-query result=successThe method also takes a block. In this case the message will be emitted twice, once at the start of the execution and once at the end. The end result might look like so:
logger.log :doing_work do # => doing-work at=start
do_some_work #
logger.log :work_done # => work-done
end # => doing-work at=finish elapsed=1.2345In case of an exception inside the block, all relevant information is logged and then the exception is re-raised.
logger.log :doing_work do # => doing-work at=start
raise ArgumentError, \ #
"something is wrong" #
end # => doing-work at=exception exception=ArgumentError message="something is wrong" elapsed=1.2345
# ArgumentError: something is wrongL2meter allows setting context for a block. It might work something like this:
def do_work_with_retries
attempt = 1
begin
logger.context attempt: attempt do
do_some_work # => doing-work attempt=1
# => doing-work attempt=2
# => doing-work attempt=3
end
rescue => error
attempt += 1
retry
end
endL2meter supports dynamic contexts as well. You can pass a proc instead of raw value in order to use it.
The example above could be re-written like this instead:
def do_work_with_retries
attempt = 1
logger.context ->{{ attempt: attempt }} do
begin
do_some_work
rescue => error
attempt +=1
retry
end
end
endIt's possible to create a dedicated copy of the logger with some specific context attached to it.
worker_logger = logger.context(component: :worker, worker_id: 123)
MyWorker.new(logger: worker_logger).run # => component=worker worker_id=123 status="doing work"There's a way to batch several calls into a single log line:
logger.batch do
logger.log foo: :bar
logger.unique :registration, "user@example.com"
logger.count :thing, 10
logger.sample :other_thing, 20
end # => foo=bar unique#registration=user@example.com count#thing=10 sample#other-thing=20Some l2met-specific metrics are supported.
logger.count :user_registered # => count#user-registered=1
logger.count :registered_users, 10 # => count#registered-users=10
logger.measure :connection_count, 20 # => measure#connection-count=20
logger.measure :db_query, 235, unit: :ms, # => measure#db-query.ms=235
logger.sample :connection_count, 20, # => sample#connection-count=235
logger.sample :db_query, 235, unit: :ms, # => sample#db-query.ms=235
logger.unique :user, "bob@example.com" # => unique#user=bob@example.comL2meter allows to append elapsed time to log messages automatically.
logger.with_elapsed do
do_work_step_1
logger.log :step_1_done # => step-1-done elapsed=1.2345
do_work_step_2
logger.log :step_2_done # => step-2-done elapsed=2.3456
endL2meter supports customizable configuration.
logger = L2meter.build do |config|
# configuration happens here
endHere's the full list of available settings.
Global context works similarly to context method, but globally:
config.context = { app_name: "my-app-name" }
# ...
logger.log foo: :bar # => app-name=my-app-name foo=barDynamic context is also supported:
config.context do
{ request_id: SecureRandom.uuid }
end
logger.log :hello # => hello request_id=4209ba28-4a7c-40d6-af69-c2c1ddf51f19
logger.log :world # => world request_id=b6836b1b-5710-4f5f-926d-91ab9988a7c1By default l2meter doesn't sort tokens before output, putting them in the order they're passed. But it's possible to sort them like so:
config.sort = true
# ...
logger.log :c, :b, :a # => a b cSource is a special parameter that'll be appended to all emitted messages.
config.source = "com.heroku.my-application.staging"
# ...
logger.log foo: :bar # => source=com.heroku.my-application.staging foo=barPrefix allows to add namespacing to measure/count/unique/sample calls.
config.prefix = "my-app"
# ...
logger.count :users, 100500 # => count#my-app.users=100500L2meter allows plugging in custom scrubbing logic that might be useful in environments where logging compliance is important to prevent accidentally leaking sensitive information.
config.scrubber = -> (key, value) do
begin
uri = URI.parse(value)
uri.password = "redacted" if uri.password
uri.to_s
rescue URI::Error
value
end
end
logger.log my_url: "https://user:password@example.com"
# => my-url="https://user:redacted@example.com"Note that returning nil value will make l2meter omit the field completely.
By default l2meter will treat key-value pairs where the value is true, false or nil differently. false and nil values will cause the whole pair to be omitted, true will cause just the key to be output:
logger.log foo: "hello", bar: true # => foo=hello bar
logger.log foo: "hello", bar: false # => foo=hello
logger.log foo: "hello", bar: nil # => foo=helloWhen the option is disabled, the full pairs are emitted:
config.compact_values = false
logger.log foo: "hello", bar: true # => foo=hello bar=true
logger.log foo: "hello", bar: false # => foo=hello bar=false
logger.log foo: "hello", bar: nil # => foo=hello bar=nullNote that "null" is output in the nil case.
There's a way to temporary silence the log emitter. This might be useful for tests for example.
logger.silence do
# logger is completely silenced
logger.log "hello world" # nothing is emitted here
end
# works normally again
logger.log :foo # => fooThe typical setup for RSpec might look like this:
RSpec.configure do |config|
config.around :each do |example|
MyLogger.silence &example
end
endNote that silence method will only suppress logging in the current thread.
It'll still produce output if you fire up a new thread. To silence it
completely, use disable! method. This will completely silence the logger
across all threads.
# spec/spec_helper.rb
MyLogger.disable!