Skip to content

Logman — Lightweight abstraction for formatted logging

License

Notifications You must be signed in to change notification settings

renderedtext/logman

Repository files navigation

Logman

Build Status

Logman introduces a unified logging format in your project. Every log line is an event that is logged to the STDOUT or to file.

Installation

Add this line to your application's Gemfile:

gem "rt-logman"

Basic Usage

To log an informative message to STDOUT, use the following code snippet:

Logman.info("Hello World")

# Output:
# {"level":"INFO","time":"2017-12-12 09:33:00 +0000","pid":10950,"message":"Hello World"}

Every log event can be extended with metadata — a hash with key value pairs:

Logman.info("Hello World", :from => "renderedtext", :to => "The World")

# Output:
# {"level":"INFO","time":"2017-12-12 09:33:21 +0000","pid":10950,"message":"Hello World","from":"renderedtext","to":"The World"}

Every log event has a severity. In the previous examples we have used info. To log an error use the following snippet:

Logman.error("Team does not exists", :owner => "renderedtext", :team_name => "z-fightes")

# Output:
# {"level":"ERROR","time":"2017-12-12 09:33:47 +0000","pid":10950,"message":"Team does not exists","owner":"renderedtext","team_name":"z-fightes"}

Logman supports multiple severity levels:

Logman.fatal("Hello")
Logman.error("Hello")
Logman.warn("Hello")
Logman.info("Hello")
Logman.debug("Hello")

Where the following hierarchy stands:

FATAL > ERROR > WARN > INFO > DEBUG

Instantiated Loggers

Logs in a class or system component usually share common metadata fields. For this purpose, a new instance of Logman can be created and pre-populated with metadata.

In the following example, we will add logs to a video processor:

class VideoProcessor

  def initialize(video)
    @logger = Logman.new

    # these fields will appear in every log event
    @logger.add(:component => "video_processor")
    @logger.add(:id => @video.id)
    @logger.add(:title => "Keyboard Cat")
  end

  def process
    @logger.info("started")

    content = load_from_disk(@video.location)
    @logger.info("loaded into memory", :size => content.length)

    compressed_content = compress(@video)
    @logger.info("compressed", :size => compressed_content.length)

    s3_path = upload_to_s3(@video)
    @logger.info("uploaded to S3", :s3_path => s3_path

    @video.update(:s3_path => s3_path)

    @logger.info("finished")
  rescue => exception
    @logger.error("failed", :exception => exception.message)

    raise
  end

end

In case of a successful processing of a video, we would see the following:

{"level":"INFO","time":"2017-12-12 09:35:19 +0000","pid":10950,"message":"started","component":"video_processor","id":9312,"title":"Keyboard Cat"}
{"level":"INFO","time":"2017-12-12 09:35:34 +0000","pid":10950,"message":"loaded into memory","component":"video_processor","id":9312,"title":"Keyboard Cat","size":41241241}
{"level":"INFO","time":"2017-12-12 09:35:44 +0000","pid":10950,"message":"compressed","component":"video_processor","id":9312,"title":"Keyboard Cat","size":1312312}
{"level":"INFO","time":"2017-12-12 09:36:08 +0000","pid":10950,"message":"uploaded to S3","component":"video_processor","id":9312,"title":"Keyboard Cat","s3_path":"s3://hehe/a.mpeg"}
{"level":"INFO","time":"2017-12-12 09:36:27 +0000","pid":10950,"message":"finished","component":"video_processor","id":9312,"title":"Keyboard Cat"}

In case of an error, we would see the following:

{"level":"INFO","time":"2017-12-12 09:35:19 +0000","pid":10950,"message":"started","component":"video_processor","id":9312,"title":"Keyboard Cat"}
{"level":"ERROR","time":"2017-12-12 09:35:34 +0000","pid":10950,"message":"failed","component":"video_processor","id":9312,"title":"Keyboard Cat","size":41241241,"exception": "Out of memory"}

Logman can receive a Ruby Logger instance to handle output. This is useful if you want to log to a file.

@logger = Logman.new(:logger => Logger.new("/tmp/out.txt"))

@logger.info("Hello World")

# => output goes to /tmp/out.txt

You can also pass an instance of Rails logger:

@logger = Logman.new(:logger => Rails.logger)

@logger.info("Hello World")

Or, you can pass an instance of another Logman. This is useful if you want to create a new Logman instance with the same fields as the previous instance:

@api_logger = Logman.new
@api_logger.add(:version => "v2")

@team_api_logger = Logman.new(:logger => @api_logger)
# team logger copied the `version` field

@team_api_logger.info("Hello")

# {"level":"INFO","time":"2017-12-12 09:39:07 +0000","pid":10950,"message":"Hello","version":"v2"}

With Logman, you can instrument data processing with a logger block. For example, if you want to log the steps in a user sign-up progress:

Logman.process("user-registration", :username => "shiroyasha") do |logger|
  user = User.create(params)
  logger.info("User Record Created")

  SigupEmail.send(user)
  logger.info("Sent signup email")

  team.add(user)
  logger.info("Added user to a team", :team_id => team.id)
end

The above will log the following information:

{"level":"INFO","time":"2017-12-12 09:40:39 +0000","pid":10950,"message":"user-registration-started","username":"shiroyasha"}
{"level":"INFO","time":"2017-12-12 09:40:39 +0000","pid":10950,"message":"User Record Created","username":"shiroyasha"}
{"level":"INFO","time":"2017-12-12 09:40:39 +0000","pid":10950,"message":"Sent signup email","username":"shiroyasha"}
{"level":"INFO","time":"2017-12-12 09:40:39 +0000","pid":10950,"message":"Added user to a team","username":"shiroyasha","team_id":21}
{"level":"INFO","time":"2017-12-12 09:40:39 +0000","pid":10950,"message":"user-registration-finished","username":"shiroyasha"}

In case of an exception, the error will be logged and re-thrown:

Logman.process("user-registration", :username => "shiroyasha") do |logger|
  user = User.create(params)
  logger.info("User Record Created")

  SigupEmail.send(user)
  logger.info("Sent signup email")

  raise "Exception"

  team.add(user)
  logger.info("Added user to a team", :team_id => team)
end
{"level":"INFO","time":"2017-12-12 09:41:27 +0000","pid":10950,"message":"user-registration-started","username":"shiroyasha"}
{"level":"INFO","time":"2017-12-12 09:41:27 +0000","pid":10950,"message":"User Record Created","username":"shiroyasha"}
{"level":"INFO","time":"2017-12-12 09:41:27 +0000","pid":10950,"message":"Sent signup email","username":"shiroyasha"}
{"level":"ERROR","time":"2017-12-12 09:41:27 +0000","pid":10950,"message":"Exception","username":"shiroyasha","type":"RuntimeError"}

Development

After checking out the repo:

  • run bundle install to install dependencies
  • run bundle exec rspec to run unit specs
  • run bundle exec rubocop to check code style
  • run bundle exec reek to check code smells

To release a new version:

  • bump the version in lib/logman/version.rb
  • run bundle exec rake release to release a new version on RubyGems

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/renderedtext/logman. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Logman project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.