Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/.bundle/
/.yardoc
/Gemfile.lock
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/
/email/
/log/
12 changes: 12 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Encoding:
Enabled: false

LineLength:
Enabled: true
Max: 128

Documentation:
Enabled: false

MethodLength:
Enabled: false
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
language: ruby
rvm:
- 2.2.3
before_install: gem install bundler -v 1.10.6
script: bundle install && bundle exec rake full_build
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
source 'https://rubygems.org'

# Specify your gem's dependencies in demo_logger.gemspec
gemspec
72 changes: 71 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,72 @@
[![Build Status](https://travis-ci.org/CivJ/demo_logger.svg?branch=zen_hw)](https://travis-ci.org/CivJ/demo_logger)

# demo_logger
Experimenting with logging features

This logging library layers additional features on top of Ruby's [logger](http://ruby-doc.org/stdlib-2.2.3/libdoc/logger/rdoc/Logger.html)

## Installation

Add this line to your application's Gemfile:

gem 'demo_logger'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install demo_logger

## Building
See tasks: `bundle exec rake -T`

Full build: `bundle exec rake full_build`

Tests only: `bundle exec rake spec`

## Usage

```
require 'demo_logger'
logger = DemoLogger::DemoLogger.new
```

## Design Overview
I use a `MultiLogger` class to dispatch messages to `FileLogger`, `StdoutLogger` and `EmailLogger`. Each of these
classes depends on Ruby's built in `Logger` class. The logic is pretty simple, each class will always relieve all
the logging calls, but will ignore or execute them according to their configuration. Please see `lib/demo_logger`
for the implementation. Tests are located at `spec/`.


## Configuration

Configuration is provided via [clean_config](https://github.com/opower/clean_config), an open source library I wrote.
Please see that project's README for details on how configuration works.

A basic example is

```
# <your-top-level-project>/config/config.yml <-- this location is required
:demo_logger:
:level: info
```

## E2E tests
The tests at `spec/e2e/core/spec/unit` show what happens when you actually include this as a separate library
into a new project. That is the appropriate location to test config value overrides and 'require' statements. Those
tests should guarantee this project will work as expected from another project.

The `Core` project is just a dummy project that depends on `demo-logger`. The only files of interest are:

* spec/e2e/core/lib/core.rb
* spec/e2e/core/spec/unit/core_spec.rb

## TODOs
Most of the obvious improvements that I am intentionally omitting are marked with TODO.

### Other omissions given that this is an interview question
* Actually sending email: It should be easy to plug this in. Let's talk about it!
* Integrating with a log aggregator (e.g. Logstash, Splunk)
* Making everything configurable
* Aging log files (the underlying library supports this)
21 changes: 21 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'
require 'rubocop/rake_task'

RSpec::Core::RakeTask.new(:spec)
RuboCop::RakeTask.new

namespace :rm do
desc 'Remove ./log files'
task :log do
FileUtils.rm_rf('log')
end

desc 'Remove ./email files'
task :email do
FileUtils.rm_rf('email')
end
end

desc 'Run style check, tests and build'
task full_build: [:'rm:log', :'rm:email', :rubocop, :spec, :build]
7 changes: 7 additions & 0 deletions config/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# debug info log warn severe

:demo_logger:
:file: info
:stdout: debug
:email: severe

36 changes: 36 additions & 0 deletions demo_logger.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'demo_logger/version'

Gem::Specification.new do |spec|
spec.name = 'demo_logger'
spec.version = DemoLogger::VERSION
spec.authors = ['john-crimmins']
spec.email = ['john.crimmins@gmail.com']

spec.summary = 'An example logger layered on top of the built in Ruby logger'
spec.description = ''
spec.homepage = 'http://github.com/CivJ/demo_logger'
spec.required_ruby_version = '~> 2.2'

# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
# delete this section to allow pushing this gem to any host.
if spec.respond_to?(:metadata)
spec.metadata['allowed_push_host'] = 'http://mygemserver.com'
else
fail 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
end

# rubocop:disable Style/RegexpLiteral
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
spec.require_paths = ['lib']

spec.add_runtime_dependency 'clean_config', '= 0.0.2'

spec.add_development_dependency 'bundler', '~> 1.10'
spec.add_development_dependency 'rake', '~> 10.0'
spec.add_development_dependency 'should_not', '~> 1.1'
spec.add_development_dependency 'rspec', '~> 3.3'
spec.add_development_dependency 'rubocop', '= 0.26.0'
end
4 changes: 4 additions & 0 deletions lib/demo_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require_relative 'demo_logger/version'
require_relative 'demo_logger/multi_logger'
require_relative 'demo_logger/file_logger'
require_relative 'demo_logger/stdout_logger'
27 changes: 27 additions & 0 deletions lib/demo_logger/email_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'logger'
require 'stringio'

module DemoLogger
# TODO: This could share a common superclass with FileLogger
# TODO: Sending emails
class EmailLogger < Logger
attr_reader :email_file
attr_accessor :send_mail

def initialize(level, send_mail = false)
log_file_dir = File.expand_path(File.join(Dir.pwd, 'email'))
FileUtils.mkdir_p(log_file_dir)
log_file_path = File.join(log_file_dir, 'logger.log')
@email_file = File.open(File.join(log_file_path), 'a')
@send_mail = send_mail

# We are setting sync = true because of the following line from the spec:
# "You need to build a framework that will continuously dump the logger output to a file."
# This sounds like an instruction to avoid buffering, so that's what we're doing.
# Obvious performance hit.
@email_file.sync = true
super(@email_file)
self.level = level || Logger::WARN
end
end
end
25 changes: 25 additions & 0 deletions lib/demo_logger/file_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require 'logger'
require 'clean_config'
require 'fileutils'

module DemoLogger
class FileLogger < Logger
attr_accessor :log_file

# @param level [String] log level for files
def initialize(level)
log_file_dir = File.expand_path(File.join(Dir.pwd, 'log'))
FileUtils.mkdir_p(log_file_dir)
log_file_path = File.join(log_file_dir, 'logger.log')
@log_file = File.open(File.join(log_file_path), 'a')

# We are setting sync = true because of the following line from the spec:
# "You need to build a framework that will continuously dump the logger output to a file."
# This sounds like an instruction to avoid buffering, so that's what we're doing.
# Obvious performance hit.
@log_file.sync = true
super(@log_file)
self.level = level || Logger::WARN
end
end
end
81 changes: 81 additions & 0 deletions lib/demo_logger/multi_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require 'logger'
require 'clean_config'
require_relative 'file_logger'
require_relative 'stdout_logger'
require_relative 'email_logger'

module DemoLogger
class MultiLogger
include CleanConfig::Configurable
attr_reader :logs

# TODO: :log and :info are the same. How should #log map onto logger's api?
LOGGER_CONST_MAP = {
debug: Logger::DEBUG, # 0
info: Logger::INFO, # 1
log: Logger::INFO,
warn: Logger::WARN, # 2
severe: Logger::FATAL # 4
}
FILE = :file
STDOUT = :stdout
EMAIL = :email

# @param config [Hash] config options
# @option config [String] :file log level for file
# @option config [String] :stdout log level for stdout
# @option config [String] :email log level for email
def initialize(config = {})
defaults = {
demo_logger: { file: 'warn', stdout: 'warn', email: 'warn' }
}
config = defaults.merge(CleanConfig::Configuration.instance).merge(config)

@logs = {}
file_level = config[:demo_logger][:file]
stdout_level = config[:demo_logger][:stdout]
email_level = config[:demo_logger][:email]
@logs[MultiLogger::FILE] = FileLogger.new(translate_level(file_level))
@logs[MultiLogger::STDOUT] = StdoutLogger.new(translate_level(stdout_level))
@logs[MultiLogger::EMAIL] = EmailLogger.new(translate_level(email_level))
end

# TODO: We could probably clean this repetition up with something clever like Forwardable.
def debug(message)
@logs.each { |_type, log| log.debug(message) }
end

def info(message)
@logs.each { |_type, log| log.info(message) }
end

# TODO: how should we map #log onto Logger's api?
def log(message)
@logs.each { |_type, log| log.info(message) }
end

def warn(message)
@logs.each { |_type, log| log.warn(message) }
end

def severe(message)
@logs.each { |_type, log| log.fatal(message) }
end

def close
@logs.each { |_type, log| log.close }
end

# rubocop:disable Style/TrailingWhitespace

private

def translate_level(level)
LOGGER_CONST_MAP[level.downcase.to_sym]
end

def from_config(key)
CleanConfig::Configuration.instance[:demo_logger][key]
end
end
end
15 changes: 15 additions & 0 deletions lib/demo_logger/stdout_logger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require 'logger'

module DemoLogger
class StdoutLogger < Logger
# @param level [String] log level for STDOUT
def initialize(level)
super(STDOUT)
self.level = level || Logger::WARN
end

# Don't close STDOUT.
def close
end
end
end
3 changes: 3 additions & 0 deletions lib/demo_logger/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module DemoLogger
VERSION = '0.1.0'
end
19 changes: 19 additions & 0 deletions spec/e2e/core/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
vendor
*.log
1 change: 1 addition & 0 deletions spec/e2e/core/.rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--require spec_helper
22 changes: 22 additions & 0 deletions spec/e2e/core/.rubocop.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#do not check for utf stamp
Encoding:
Enabled: false

LineLength:
Enabled: true
Max: 128

# The rubocop Documentation cop is a duplicate of the Reek IrresponsibleModule
# check.
Documentation:
Enabled: false

CaseIndentation:
Enabled: false

# MethodLength is superseded by Reek's TooManyStatements smell-finder.
MethodLength:
Enabled: false

CyclomaticComplexity:
Max: 10
5 changes: 5 additions & 0 deletions spec/e2e/core/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
source :rubygems

# Specify your gem's dependencies in core.gemspec
gemspec
gem 'demo_logger', path: '../../..'
1 change: 1 addition & 0 deletions spec/e2e/core/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require_relative 'lib/tasks'
4 changes: 4 additions & 0 deletions spec/e2e/core/config/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
:demo_logger:
:file: debug
:email: debug
:stdout: debug
Loading