logstasher - initial gem2deb package.
authorFrantišek Dvořák <valtri@civ.zcu.cz>
Wed, 12 Mar 2014 09:21:45 +0000 (10:21 +0100)
committerFrantišek Dvořák <valtri@civ.zcu.cz>
Wed, 12 Mar 2014 09:21:45 +0000 (10:21 +0100)
30 files changed:
ruby-logstasher/.gitignore [new file with mode: 0644]
ruby-logstasher/.rspec [new file with mode: 0644]
ruby-logstasher/.travis.yml [new file with mode: 0644]
ruby-logstasher/Gemfile [new file with mode: 0644]
ruby-logstasher/Guardfile [new file with mode: 0644]
ruby-logstasher/README.md [new file with mode: 0644]
ruby-logstasher/Rakefile [new file with mode: 0644]
ruby-logstasher/checksums.yaml.gz [new file with mode: 0644]
ruby-logstasher/debian/changelog [new file with mode: 0644]
ruby-logstasher/debian/compat [new file with mode: 0644]
ruby-logstasher/debian/control [new file with mode: 0644]
ruby-logstasher/debian/copyright [new file with mode: 0644]
ruby-logstasher/debian/ruby-logstasher.docs [new file with mode: 0644]
ruby-logstasher/debian/ruby-test-files.yaml [new file with mode: 0644]
ruby-logstasher/debian/rules [new file with mode: 0755]
ruby-logstasher/debian/source/format [new file with mode: 0644]
ruby-logstasher/debian/watch [new file with mode: 0644]
ruby-logstasher/lib/logstasher.rb [new file with mode: 0644]
ruby-logstasher/lib/logstasher/device/redis.rb [new file with mode: 0644]
ruby-logstasher/lib/logstasher/log_subscriber.rb [new file with mode: 0644]
ruby-logstasher/lib/logstasher/rails_ext/action_controller/metal/instrumentation.rb [new file with mode: 0644]
ruby-logstasher/lib/logstasher/rails_ext/rack/logger.rb [new file with mode: 0644]
ruby-logstasher/lib/logstasher/railtie.rb [new file with mode: 0644]
ruby-logstasher/lib/logstasher/version.rb [new file with mode: 0644]
ruby-logstasher/logstasher.gemspec [new file with mode: 0644]
ruby-logstasher/metadata.yml [new file with mode: 0644]
ruby-logstasher/spec/lib/logstasher/device/redis_spec.rb [new file with mode: 0644]
ruby-logstasher/spec/lib/logstasher/log_subscriber_spec.rb [new file with mode: 0644]
ruby-logstasher/spec/lib/logstasher_spec.rb [new file with mode: 0644]
ruby-logstasher/spec/spec_helper.rb [new file with mode: 0644]

diff --git a/ruby-logstasher/.gitignore b/ruby-logstasher/.gitignore
new file mode 100644 (file)
index 0000000..4af26ce
--- /dev/null
@@ -0,0 +1,5 @@
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+.idea
\ No newline at end of file
diff --git a/ruby-logstasher/.rspec b/ruby-logstasher/.rspec
new file mode 100644 (file)
index 0000000..5f16476
--- /dev/null
@@ -0,0 +1,2 @@
+--color
+--format progress
diff --git a/ruby-logstasher/.travis.yml b/ruby-logstasher/.travis.yml
new file mode 100644 (file)
index 0000000..b523bb9
--- /dev/null
@@ -0,0 +1,12 @@
+language: ruby
+rvm:
+  - 1.9.3
+  - 2.0.0
+
+env:
+  - "RAILS_VERSION=4.0"
+  - "RAILS_VERSION=3.2"
+  - "RAILS_VERSION=3.1"
+  - "RAILS_VERSION=3.0"
+
+script: bundle exec rspec
diff --git a/ruby-logstasher/Gemfile b/ruby-logstasher/Gemfile
new file mode 100644 (file)
index 0000000..2ca61bf
--- /dev/null
@@ -0,0 +1,15 @@
+source "https://rubygems.org"
+
+# Specify your gem's dependencies in logstasher.gemspec
+gemspec
+
+group :test do
+  gem 'growl'
+  gem 'guard'
+  gem 'guard-rspec'
+  gem 'rails', "~> #{ENV["RAILS_VERSION"] || "3.2.0"}"
+  gem 'rb-fsevent', '~> 0.9'
+  gem 'rcov', :platforms => :mri_18
+  gem 'redis', :require => false
+  gem 'simplecov', :platforms => :mri_19, :require => false
+end
diff --git a/ruby-logstasher/Guardfile b/ruby-logstasher/Guardfile
new file mode 100644 (file)
index 0000000..4dda56e
--- /dev/null
@@ -0,0 +1,10 @@
+# A sample Guardfile
+# More info at https://github.com/guard/guard#readme
+interactor :simple
+
+guard 'rspec', :version => 2 do
+  watch(%r{^spec/.+_spec\.rb$})
+  watch(%r{^lib/(.+)\.rb$})     { |m| "spec/lib/#{m[1]}_spec.rb" }
+  watch('spec/spec_helper.rb')  { "spec" }
+end
+
diff --git a/ruby-logstasher/README.md b/ruby-logstasher/README.md
new file mode 100644 (file)
index 0000000..a302ac1
--- /dev/null
@@ -0,0 +1,104 @@
+# Logstasher [![Gem Version](https://badge.fury.io/rb/logstasher.png)](http://badge.fury.io/rb/logstasher) [![Build Status](https://secure.travis-ci.org/shadabahmed/logstasher.png)](https://secure.travis-ci.org/shadabahmed/logstasher)
+### Awesome Logging for Rails !!
+
+This gem is heavily inspired from [lograge](https://github.com/roidrage/lograge), but it's focused on one thing and one thing only. That's making your logs awesome like this:
+
+[![Awesome Logs](http://i.imgur.com/zZXWQNp.png)](http://i.imgur.com/zZXWQNp.png)
+
+How it's done ?
+
+By, using these awesome tools:
+* [Logstash](http://logstash.net) - Store and index your logs
+* [Kibana](http://kibana.org/) - for awesome visualization. This is optional though, and you can use any other visualizer
+
+To know how to setup these tools - visit my [blog](http://shadabahmed.com/blog/2013/04/30/logstasher-for-awesome-rails-logging)
+
+## About logstasher
+
+This gem purely focuses on how to generate logstash compatible logs i.e. *logstash json event format*,  without any overhead. Infact, logstasher logs to a separate log file named `logstash_<environment>.log`.
+The reason for this separation:
+ * To have a pure json log file
+ * Prevent any logger messages(e.g. info) getting into our pure json logs
+
+Before **logstasher** :
+
+```
+Started GET "/login" for 10.109.10.135 at 2013-04-30 08:59:01 -0400
+Processing by SessionsController#new as HTML
+  Rendered sessions/new.html.haml within layouts/application (4.3ms)
+  Rendered shared/_javascript.html.haml (0.6ms)
+  Rendered shared/_flashes.html.haml (0.2ms)
+  Rendered shared/_header.html.haml (52.9ms)
+  Rendered shared/_title.html.haml (0.2ms)
+  Rendered shared/_footer.html.haml (0.2ms)
+Completed 200 OK in 532ms (Views: 62.4ms | ActiveRecord: 0.0ms | ND API: 0.0ms)
+```
+
+After **logstasher**:
+
+```
+{"@source":"unknown","@tags":["request"],"@fields":{"method":"GET","path":"/","format":"html","controller":"file_servers"
+,"action":"index","status":200,"duration":28.34,"view":25.96,"db":0.88,"ip":"127.0.0.1","route":"file_servers#index",
+"parameters":"","ndapi_time":null,"uuid":"e81ecd178ed3b591099f4d489760dfb6","user":"shadab_ahmed@abc.com",
+"site":"internal"},"@timestamp":"2013-04-30T13:00:46.354500+00:00"}
+```
+
+By default, the older format rails request logs are disabled, though you can enable them.
+
+## Installation
+
+In your Gemfile:
+
+    gem 'logstasher'
+
+### Configure your `<environment>.rb` e.g. `development.rb`
+
+    # Enable the logstasher logs for the current environment
+    config.logstasher.enabled = true
+
+    # This line is optional if you do not want to suppress app logs in your <environment>.log
+    config.logstasher.suppress_app_log = false
+
+    # This line is optional, it allows you to set a custom value for the @source field of the log event
+    config.logstasher.source = 'your.arbitrary.source'
+
+## Logging params hash
+
+Logstasher can be configured to log the contents of the params hash.  When enabled, the contents of the params hash (minus the ActionController internal params)
+will be added to the log as a deep hash.  This can cause conflicts within the Elasticsearch mappings though, so should be enabled with care.  Conflicts will occur
+if different actions (or even different applications logging to the same Elasticsearch cluster) use the same params key, but with a different data type (e.g. a
+string vs. a hash).  This can lead to lost log entries.  Enabling this can also significantly increase the size of the Elasticsearch indexes.
+
+To enable this, add the following to your `<environment>.rb`
+
+    # Enable logging of controller params
+    config.logstasher.log_controller_parameters = true
+
+## Adding custom fields to the log
+
+Since some fields are very specific to your application for e.g. *user_name*, so it is left upto you, to add them. Here's how to add those fields to the logs:
+
+    # Create a file - config/initializers/logstasher.rb
+
+    if LogStasher.enabled
+      LogStasher.add_custom_fields do |fields|
+        # This block is run in application_controller context,
+        # so you have access to all controller methods
+        fields[:user] = current_user && current_user.mail
+        fields[:site] = request.path =~ /^\/api/ ? 'api' : 'user'
+
+        # If you are using custom instrumentation, just add it to logstasher custom fields
+        LogStasher.custom_fields << :myapi_runtime
+      end
+    end
+
+## Versions
+All versions require Rails 3.0.x and higher and Ruby 1.9.2+. Tested on Rails 4 and Ruby 2.0
+
+## Development
+ - Run tests - `rake`
+ - Generate test coverage report - `rake coverage`. Coverage report path - coverage/index.html
+
+## Copyright
+
+Copyright (c) 2013 Shadab Ahmed, released under the MIT license
diff --git a/ruby-logstasher/Rakefile b/ruby-logstasher/Rakefile
new file mode 100644 (file)
index 0000000..0cc03a1
--- /dev/null
@@ -0,0 +1,50 @@
+require "bundler/gem_tasks"
+require 'rspec/core/rake_task'
+
+# Add default task. When you type just rake command this would run. Travis CI runs this. Making this run spec
+desc 'Default: run specs.'
+task :default => :spec
+
+# Defining spec task for running spec
+desc "Run specs"
+RSpec::Core::RakeTask.new('spec') do |spec|
+  # Pattern filr for spec files to run. This is default btw.
+  spec.pattern = "./spec/**/*_spec.rb"
+end
+
+# Run the rdoc task to generate rdocs for this gem
+require 'rdoc/task'
+RDoc::Task.new do |rdoc|
+  require "logstasher/version"
+  version = LogStasher::VERSION
+
+  rdoc.rdoc_dir = 'rdoc'
+  rdoc.title = "logstasher #{version}"
+  rdoc.rdoc_files.include('README*')
+  rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+# Code coverage tasks. Different for Ruby 1.8 vs 1.9
+if RUBY_VERSION =~ /^1\.8/
+  # Ruby 1.8 uses rcov for code coverage
+  RSpec::Core::RakeTask.new(:coverage) do |spec|
+    spec.pattern = 'spec/**/*_spec.rb'
+    spec.rcov = true
+    spec.rcov_opts = %w{--exclude pkg\/,spec\/,features\/}
+  end
+else
+  # Ruby 1.9+ using simplecov. Note: Simplecov config defined in spec_helper
+  desc "Code coverage detail"
+  task :coverage do
+    ENV['COVERAGE'] = "true"
+    Rake::Task['spec'].execute
+  end
+end
+
+task :console do
+  require 'irb'
+  require 'irb/completion'
+  require 'logstasher'
+  ARGV.clear
+  IRB.start
+end
diff --git a/ruby-logstasher/checksums.yaml.gz b/ruby-logstasher/checksums.yaml.gz
new file mode 100644 (file)
index 0000000..907fcb1
Binary files /dev/null and b/ruby-logstasher/checksums.yaml.gz differ
diff --git a/ruby-logstasher/debian/changelog b/ruby-logstasher/debian/changelog
new file mode 100644 (file)
index 0000000..06cdc28
--- /dev/null
@@ -0,0 +1,5 @@
+ruby-logstasher (0.5.0-1) UNRELEASED; urgency=medium
+
+  * Initial release (Closes: #nnnn)
+
+ -- MAINTAINER <valtri@myriad14.zcu.cz>  Wed, 12 Mar 2014 10:19:53 +0100
diff --git a/ruby-logstasher/debian/compat b/ruby-logstasher/debian/compat
new file mode 100644 (file)
index 0000000..7f8f011
--- /dev/null
@@ -0,0 +1 @@
+7
diff --git a/ruby-logstasher/debian/control b/ruby-logstasher/debian/control
new file mode 100644 (file)
index 0000000..20acdb1
--- /dev/null
@@ -0,0 +1,19 @@
+Source: ruby-logstasher
+Section: ruby
+Priority: optional
+Maintainer: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers@lists.alioth.debian.org>
+Uploaders:  <>
+Build-Depends: debhelper (>= 7.0.50~), gem2deb (>= 0.6.1~)
+Standards-Version: 3.9.4
+#Vcs-Git: git://anonscm.debian.org/pkg-ruby-extras/ruby-logstasher.git
+#Vcs-Browser: http://anonscm.debian.org/gitweb/?p=pkg-ruby-extras/ruby-logstasher.git;a=summary
+Homepage: https://github.com/shadabahmed/logstasher
+XS-Ruby-Versions: all
+
+Package: ruby-logstasher
+Architecture: all
+XB-Ruby-Versions: ${ruby:Versions}
+Depends: ${shlibs:Depends}, ${misc:Depends}, ruby | ruby-interpreter
+# logstash-event (~> 1.1.0), rspec (>= 0, development), bundler (>= 1.0.0, development), rails (>= 3.0, development)
+Description: Awesome rails logs
+ Awesome rails logs
diff --git a/ruby-logstasher/debian/copyright b/ruby-logstasher/debian/copyright
new file mode 100644 (file)
index 0000000..3889113
--- /dev/null
@@ -0,0 +1,35 @@
+Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: logstasher
+Source: FIXME <http://example.com/>
+
+Files: *
+Copyright: <years> <put author's name and email here>
+           <years> <likewise for another author>
+License: GPL-2+ (FIXME)
+
+Files: debian/*
+Copyright: 2014  <>
+License: GPL-2+ (FIXME)
+Comment: the Debian packaging is licensed under the same terms as the original package.
+
+License: GPL-2+ (FIXME)
+ This program is free software; you can redistribute it
+ and/or modify it under the terms of the GNU General Public
+ License as published by the Free Software Foundation; either
+ version 2 of the License, or (at your option) any later
+ version.
+ .
+ This program is distributed in the hope that it will be
+ useful, but WITHOUT ANY WARRANTY; without even the implied
+ warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ PURPOSE.  See the GNU General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU General Public
+ License along with this package; if not, write to the Free
+ Software Foundation, Inc., 51 Franklin St, Fifth Floor,
+ Boston, MA  02110-1301 USA
+ .
+ On Debian systems, the full text of the GNU General Public
+ License version 2 can be found in the file
+ `/usr/share/common-licenses/GPL-2'.
diff --git a/ruby-logstasher/debian/ruby-logstasher.docs b/ruby-logstasher/debian/ruby-logstasher.docs
new file mode 100644 (file)
index 0000000..07b3c9e
--- /dev/null
@@ -0,0 +1,2 @@
+# FIXME: READMEs found
+# README.md
diff --git a/ruby-logstasher/debian/ruby-test-files.yaml b/ruby-logstasher/debian/ruby-test-files.yaml
new file mode 100644 (file)
index 0000000..e85363c
--- /dev/null
@@ -0,0 +1,5 @@
+---
+- spec/lib/logstasher/device/redis_spec.rb
+- spec/lib/logstasher/log_subscriber_spec.rb
+- spec/lib/logstasher_spec.rb
+- spec/spec_helper.rb
diff --git a/ruby-logstasher/debian/rules b/ruby-logstasher/debian/rules
new file mode 100755 (executable)
index 0000000..82ddc0c
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/make -f
+#export DH_VERBOSE=1
+#
+# Uncomment to ignore all test failures (but the tests will run anyway)
+#export DH_RUBY_IGNORE_TESTS=all
+#
+# Uncomment to ignore some test failures (but the tests will run anyway).
+# Valid values:
+#export DH_RUBY_IGNORE_TESTS=ruby1.9.1 ruby2.0 require-rubygems
+#
+# If you need to specify the .gemspec (eg there is more than one)
+#export DH_RUBY_GEMSPEC=gem.gemspec
+
+%:
+       dh $@ --buildsystem=ruby --with ruby
diff --git a/ruby-logstasher/debian/source/format b/ruby-logstasher/debian/source/format
new file mode 100644 (file)
index 0000000..163aaf8
--- /dev/null
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/ruby-logstasher/debian/watch b/ruby-logstasher/debian/watch
new file mode 100644 (file)
index 0000000..2f6962a
--- /dev/null
@@ -0,0 +1,2 @@
+version=3
+http://pkg-ruby-extras.alioth.debian.org/cgi-bin/gemwatch/logstasher .*/logstasher-(.*).tar.gz
diff --git a/ruby-logstasher/lib/logstasher.rb b/ruby-logstasher/lib/logstasher.rb
new file mode 100644 (file)
index 0000000..e3887ec
--- /dev/null
@@ -0,0 +1,107 @@
+require 'logstasher/version'
+require 'logstasher/log_subscriber'
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/string/inflections'
+require 'active_support/ordered_options'
+
+module LogStasher
+  extend self
+  attr_accessor :logger, :enabled, :log_controller_parameters, :source
+  # Setting the default to 'unknown' to define the default behaviour
+  @source = 'unknown'
+
+  def remove_existing_log_subscriptions
+    ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
+      case subscriber
+        when ActionView::LogSubscriber
+          unsubscribe(:action_view, subscriber)
+        when ActionController::LogSubscriber
+          unsubscribe(:action_controller, subscriber)
+      end
+    end
+  end
+
+  def unsubscribe(component, subscriber)
+    events = subscriber.public_methods(false).reject{ |method| method.to_s == 'call' }
+    events.each do |event|
+      ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{component}").each do |listener|
+        if listener.instance_variable_get('@delegate') == subscriber
+          ActiveSupport::Notifications.unsubscribe listener
+        end
+      end
+    end
+  end
+
+  def add_default_fields_to_payload(payload, request)
+    payload[:ip] = request.remote_ip
+    payload[:route] = "#{request.params[:controller]}##{request.params[:action]}"
+    self.custom_fields += [:ip, :route]
+    if self.log_controller_parameters
+      payload[:parameters] = payload[:params].except(*ActionController::LogSubscriber::INTERNAL_PARAMS)
+      self.custom_fields += [:parameters]
+    end
+  end
+
+  def add_custom_fields(&block)
+    ActionController::Metal.send(:define_method, :logtasher_add_custom_fields_to_payload, &block)
+    ActionController::Base.send(:define_method, :logtasher_add_custom_fields_to_payload, &block)
+  end
+
+  def setup(app)
+    app.config.action_dispatch.rack_cache[:verbose] = false if app.config.action_dispatch.rack_cache
+    # Path instrumentation class to insert our hook
+    require 'logstasher/rails_ext/action_controller/metal/instrumentation'
+    require 'logstash-event'
+    self.suppress_app_logs(app)
+    LogStasher::RequestLogSubscriber.attach_to :action_controller
+    self.logger = app.config.logstasher.logger || new_logger("#{Rails.root}/log/logstash_#{Rails.env}.log")
+    self.logger.level = app.config.logstasher.log_level || Logger::WARN
+    self.source = app.config.logstasher.source unless app.config.logstasher.source.nil?
+    self.enabled = true
+    self.log_controller_parameters = !! app.config.logstasher.log_controller_parameters
+  end
+
+  def suppress_app_logs(app)
+    if configured_to_suppress_app_logs?(app)
+      require 'logstasher/rails_ext/rack/logger'
+      LogStasher.remove_existing_log_subscriptions
+    end
+  end
+
+  def configured_to_suppress_app_logs?(app)
+    # This supports both spellings: "suppress_app_log" and "supress_app_log"
+    !!(app.config.logstasher.suppress_app_log.nil? ? app.config.logstasher.supress_app_log : app.config.logstasher.suppress_app_log)
+  end
+
+  def custom_fields
+    Thread.current[:logstasher_custom_fields] ||= []
+  end
+
+  def custom_fields=(val)
+    Thread.current[:logstasher_custom_fields] = val
+  end
+
+  def log(severity, msg)
+    if self.logger && self.logger.send("#{severity}?")
+      event = LogStash::Event.new('@source' => self.source, '@fields' => {:message => msg, :level => severity}, '@tags' => ['log'])
+      self.logger.send severity, event.to_json
+    end
+  end
+
+  %w( fatal error warn info debug unknown ).each do |severity|
+    eval <<-EOM, nil, __FILE__, __LINE__ + 1
+      def #{severity}(msg)
+        self.log(:#{severity}, msg)
+      end
+    EOM
+  end
+
+  private
+
+  def new_logger(path)
+    FileUtils.touch path # prevent autocreate messages in log
+    Logger.new path
+  end
+end
+
+require 'logstasher/railtie' if defined?(Rails)
diff --git a/ruby-logstasher/lib/logstasher/device/redis.rb b/ruby-logstasher/lib/logstasher/device/redis.rb
new file mode 100644 (file)
index 0000000..eb6d5ce
--- /dev/null
@@ -0,0 +1,64 @@
+require 'redis'
+
+module LogStasher
+  module Device
+    class Redis
+
+      attr_reader :options, :redis
+
+      def initialize(options = {})
+        @options = default_options.merge(options)
+        validate_options
+        configure_redis
+      end
+
+      def data_type
+        options[:data_type]
+      end
+
+      def key
+        options[:key]
+      end
+
+      def redis_options
+        unless @redis_options
+          default_keys = default_options.keys
+          @redis_options = options.select { |k, v| !default_keys.include?(k) }
+        end
+
+        @redis_options
+      end
+
+      def write(log)
+        case data_type
+        when 'list'
+          redis.rpush(key, log)
+        when 'channel'
+          redis.publish(key, log)
+        else
+          fail "Unknown data type #{data_type}"
+        end
+      end
+
+      def close
+        redis.quit
+      end
+
+      private
+
+      def configure_redis
+        @redis = ::Redis.new(redis_options)
+      end
+
+      def default_options
+          { key: 'logstash', data_type: 'list' }
+      end
+
+      def validate_options
+        unless ['list', 'channel'].include?(options[:data_type])
+          fail 'Expected :data_type to be either "list" or "channel"'
+        end
+      end
+    end
+  end
+end
diff --git a/ruby-logstasher/lib/logstasher/log_subscriber.rb b/ruby-logstasher/lib/logstasher/log_subscriber.rb
new file mode 100644 (file)
index 0000000..3227e26
--- /dev/null
@@ -0,0 +1,96 @@
+require 'active_support/core_ext/class/attribute'
+require 'active_support/log_subscriber'
+
+module LogStasher
+  class RequestLogSubscriber < ActiveSupport::LogSubscriber
+    def process_action(event)
+      payload = event.payload
+
+      data      = extract_request(payload)
+      data.merge! extract_status(payload)
+      data.merge! runtimes(event)
+      data.merge! location(event)
+      data.merge! extract_exception(payload)
+      data.merge! extract_custom_fields(payload)
+
+      tags = ['request']
+      tags.push('exception') if payload[:exception]
+      event = LogStash::Event.new('@source' => LogStasher.source, '@fields' => data, '@tags' => tags)
+      LogStasher.logger << event.to_json + "\n"
+    end
+
+    def redirect_to(event)
+      Thread.current[:logstasher_location] = event.payload[:location]
+    end
+
+    private
+
+    def extract_request(payload)
+      {
+        :method => payload[:method],
+        :path => extract_path(payload),
+        :format => extract_format(payload),
+        :controller => payload[:params]['controller'],
+        :action => payload[:params]['action']
+      }
+    end
+
+    def extract_path(payload)
+      payload[:path].split("?").first
+    end
+
+    def extract_format(payload)
+      if ::ActionPack::VERSION::MAJOR == 3 && ::ActionPack::VERSION::MINOR == 0
+        payload[:formats].first
+      else
+        payload[:format]
+      end
+    end
+
+    def extract_status(payload)
+      if payload[:status]
+        { :status => payload[:status].to_i }
+      else
+        { :status => 0 }
+      end
+    end
+
+    def runtimes(event)
+      {
+        :duration => event.duration,
+        :view => event.payload[:view_runtime],
+        :db => event.payload[:db_runtime]
+      }.inject({}) do |runtimes, (name, runtime)|
+        runtimes[name] = runtime.to_f.round(2) if runtime
+        runtimes
+      end
+    end
+
+    def location(event)
+      if location = Thread.current[:logstasher_location]
+        Thread.current[:logstasher_location] = nil
+        { :location => location }
+      else
+        {}
+      end
+    end
+
+    # Monkey patching to enable exception logging
+    def extract_exception(payload)
+      if payload[:exception]
+        exception, message = payload[:exception]
+        status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception)
+        message = "#{exception}\n#{message}\n#{($!.backtrace.join("\n"))}"
+        { :status => status, :error => message }
+      else
+        {}
+      end
+    end
+
+    def extract_custom_fields(payload)
+      custom_fields = (!LogStasher.custom_fields.empty? && payload.extract!(*LogStasher.custom_fields)) || {}
+      LogStasher.custom_fields.clear
+      custom_fields
+    end
+  end
+end
diff --git a/ruby-logstasher/lib/logstasher/rails_ext/action_controller/metal/instrumentation.rb b/ruby-logstasher/lib/logstasher/rails_ext/action_controller/metal/instrumentation.rb
new file mode 100644 (file)
index 0000000..dbfd329
--- /dev/null
@@ -0,0 +1,35 @@
+module ActionController
+  module Instrumentation
+    def process_action(*args)
+      raw_payload = {
+          :controller => self.class.name,
+          :action     => self.action_name,
+          :params     => request.filtered_parameters,
+          :format     => request.format.try(:ref),
+          :method     => request.method,
+          :path       => (request.fullpath rescue "unknown")
+      }
+
+      LogStasher.add_default_fields_to_payload(raw_payload, request)
+
+      ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
+
+      ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
+        result = super
+        
+        if self.respond_to?(:logtasher_add_custom_fields_to_payload)
+          before_keys = raw_payload.keys.clone
+          logtasher_add_custom_fields_to_payload(raw_payload)
+          after_keys = raw_payload.keys
+          # Store all extra keys added to payload hash in payload itself. This is a thread safe way
+          LogStasher.custom_fields += after_keys - before_keys
+        end
+        
+        payload[:status] = response.status
+        append_info_to_payload(payload)
+        result
+      end
+    end
+
+  end
+end
diff --git a/ruby-logstasher/lib/logstasher/rails_ext/rack/logger.rb b/ruby-logstasher/lib/logstasher/rails_ext/rack/logger.rb
new file mode 100644 (file)
index 0000000..846ea74
--- /dev/null
@@ -0,0 +1,24 @@
+require 'rails/rack/logger'
+
+module Rails
+  module Rack
+    # Overwrites defaults of Rails::Rack::Logger that cause
+    # unnecessary logging.
+    # This effectively removes the log lines from the log
+    # that say:
+    # Started GET / for 192.168.2.1...
+    class Logger
+      # Overwrites Rails 3.2 code that logs new requests
+      def call_app(*args)
+        env = args.last
+        @app.call(env)
+      ensure
+        ActiveSupport::LogSubscriber.flush_all!
+      end
+
+      # Overwrites Rails 3.0/3.1 code that logs new requests
+      def before_dispatch(env)
+      end
+    end
+  end
+end
diff --git a/ruby-logstasher/lib/logstasher/railtie.rb b/ruby-logstasher/lib/logstasher/railtie.rb
new file mode 100644 (file)
index 0000000..911ab2f
--- /dev/null
@@ -0,0 +1,14 @@
+require 'rails/railtie'
+require 'action_view/log_subscriber'
+require 'action_controller/log_subscriber'
+
+module LogStasher
+  class Railtie < Rails::Railtie
+    config.logstasher = ActiveSupport::OrderedOptions.new
+    config.logstasher.enabled = false
+
+    initializer :logstasher, :before => :load_config_initializers do |app|
+      LogStasher.setup(app) if app.config.logstasher.enabled
+    end
+  end
+end
diff --git a/ruby-logstasher/lib/logstasher/version.rb b/ruby-logstasher/lib/logstasher/version.rb
new file mode 100644 (file)
index 0000000..92270ca
--- /dev/null
@@ -0,0 +1,3 @@
+module LogStasher
+  VERSION = "0.5.0"
+end
diff --git a/ruby-logstasher/logstasher.gemspec b/ruby-logstasher/logstasher.gemspec
new file mode 100644 (file)
index 0000000..2f463f2
--- /dev/null
@@ -0,0 +1,26 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "logstasher/version"
+
+Gem::Specification.new do |s|
+  s.name        = "logstasher"
+  s.version     = LogStasher::VERSION
+  s.authors     = ["Shadab Ahmed"]
+  s.email       = ["shadab.ansari@gmail.com"]
+  s.homepage    = "https://github.com/shadabahmed/logstasher"
+  s.summary     = %q{Awesome rails logs}
+  s.description = %q{Awesome rails logs}
+
+  s.rubyforge_project = "logstasher"
+
+  s.files         = `git ls-files`.split("\n")
+  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
+  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+  s.require_paths = ["lib"]
+  s.add_runtime_dependency "logstash-event", ["~> 1.1.0"]
+
+  # specify any dependencies here; for example:
+  s.add_development_dependency "rspec"
+  s.add_development_dependency("bundler", [">= 1.0.0"])
+  s.add_development_dependency("rails", [">= 3.0"])
+end
diff --git a/ruby-logstasher/metadata.yml b/ruby-logstasher/metadata.yml
new file mode 100644 (file)
index 0000000..34e73b8
--- /dev/null
@@ -0,0 +1,122 @@
+--- !ruby/object:Gem::Specification
+name: logstasher
+version: !ruby/object:Gem::Version
+  version: 0.5.0
+platform: ruby
+authors:
+- Shadab Ahmed
+autorequire: 
+bindir: bin
+cert_chain: []
+date: 2014-03-07 00:00:00.000000000 Z
+dependencies:
+- !ruby/object:Gem::Dependency
+  name: logstash-event
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ~>
+      - !ruby/object:Gem::Version
+        version: 1.1.0
+  type: :runtime
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ~>
+      - !ruby/object:Gem::Version
+        version: 1.1.0
+- !ruby/object:Gem::Dependency
+  name: rspec
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ! '>='
+      - !ruby/object:Gem::Version
+        version: '0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ! '>='
+      - !ruby/object:Gem::Version
+        version: '0'
+- !ruby/object:Gem::Dependency
+  name: bundler
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ! '>='
+      - !ruby/object:Gem::Version
+        version: 1.0.0
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ! '>='
+      - !ruby/object:Gem::Version
+        version: 1.0.0
+- !ruby/object:Gem::Dependency
+  name: rails
+  requirement: !ruby/object:Gem::Requirement
+    requirements:
+    - - ! '>='
+      - !ruby/object:Gem::Version
+        version: '3.0'
+  type: :development
+  prerelease: false
+  version_requirements: !ruby/object:Gem::Requirement
+    requirements:
+    - - ! '>='
+      - !ruby/object:Gem::Version
+        version: '3.0'
+description: Awesome rails logs
+email:
+- shadab.ansari@gmail.com
+executables: []
+extensions: []
+extra_rdoc_files: []
+files:
+- .gitignore
+- .rspec
+- .travis.yml
+- Gemfile
+- Guardfile
+- README.md
+- Rakefile
+- lib/logstasher.rb
+- lib/logstasher/device/redis.rb
+- lib/logstasher/log_subscriber.rb
+- lib/logstasher/rails_ext/action_controller/metal/instrumentation.rb
+- lib/logstasher/rails_ext/rack/logger.rb
+- lib/logstasher/railtie.rb
+- lib/logstasher/version.rb
+- logstasher.gemspec
+- spec/lib/logstasher/device/redis_spec.rb
+- spec/lib/logstasher/log_subscriber_spec.rb
+- spec/lib/logstasher_spec.rb
+- spec/spec_helper.rb
+homepage: https://github.com/shadabahmed/logstasher
+licenses: []
+metadata: {}
+post_install_message: 
+rdoc_options: []
+require_paths:
+- lib
+required_ruby_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ! '>='
+    - !ruby/object:Gem::Version
+      version: '0'
+required_rubygems_version: !ruby/object:Gem::Requirement
+  requirements:
+  - - ! '>='
+    - !ruby/object:Gem::Version
+      version: '0'
+requirements: []
+rubyforge_project: logstasher
+rubygems_version: 2.1.4
+signing_key: 
+specification_version: 4
+summary: Awesome rails logs
+test_files:
+- spec/lib/logstasher/device/redis_spec.rb
+- spec/lib/logstasher/log_subscriber_spec.rb
+- spec/lib/logstasher_spec.rb
+- spec/spec_helper.rb
diff --git a/ruby-logstasher/spec/lib/logstasher/device/redis_spec.rb b/ruby-logstasher/spec/lib/logstasher/device/redis_spec.rb
new file mode 100644 (file)
index 0000000..59ca325
--- /dev/null
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+require 'logstasher/device/redis'
+
+describe LogStasher::Device::Redis do
+
+  let(:redis_mock) { double('Redis') }
+
+  let(:default_options) {{
+    key: 'logstash',
+    data_type: 'list'
+  }}
+
+  it 'has default options' do
+    device = LogStasher::Device::Redis.new
+    device.options.should eq(default_options)
+  end
+
+  it 'creates a redis instance' do
+    ::Redis.should_receive(:new).with({})
+    LogStasher::Device::Redis.new()
+  end
+
+  it 'assumes unknown options are for redis' do
+    ::Redis.should_receive(:new).with(hash_including(db: '0'))
+    device = LogStasher::Device::Redis.new(db: '0')
+    device.redis_options.should eq(db: '0')
+  end
+
+  it 'has a key' do
+    device = LogStasher::Device::Redis.new(key: 'the_key')
+    device.key.should eq 'the_key'
+  end
+
+  it 'has a data_type' do
+    device = LogStasher::Device::Redis.new(data_type: 'channel')
+    device.data_type.should eq 'channel'
+  end
+
+  it 'does not allow unsupported data types' do
+    expect {
+      device = LogStasher::Device::Redis.new(data_type: 'blargh')
+    }.to raise_error()
+  end
+
+  it 'quits the redis connection on #close' do
+    device = LogStasher::Device::Redis.new
+    device.redis.should_receive(:quit)
+    device.close
+  end
+
+  it 'works as a logger device' do
+    device = LogStasher::Device::Redis.new
+    device.should_receive(:write).with('blargh')
+    logger = Logger.new(device)
+    logger << 'blargh'
+  end
+
+  describe '#write' do
+    it "rpushes logs onto a list" do
+      device = LogStasher::Device::Redis.new(data_type: 'list')
+      device.redis.should_receive(:rpush).with('logstash', 'the log')
+      device.write('the log')
+    end
+
+    it "rpushes logs onto a custom key" do
+      device = LogStasher::Device::Redis.new(data_type: 'list', key: 'custom')
+      device.redis.should_receive(:rpush).with('custom', 'the log')
+      device.write('the log')
+    end
+
+    it "publishes logs onto a channel" do
+      device = LogStasher::Device::Redis.new(data_type: 'channel', key: 'custom')
+      device.redis.should_receive(:publish).with('custom', 'the log')
+      device.write('the log')
+    end
+  end
+
+end
diff --git a/ruby-logstasher/spec/lib/logstasher/log_subscriber_spec.rb b/ruby-logstasher/spec/lib/logstasher/log_subscriber_spec.rb
new file mode 100644 (file)
index 0000000..f73fa4f
--- /dev/null
@@ -0,0 +1,190 @@
+require 'spec_helper'
+
+describe LogStasher::RequestLogSubscriber do
+  let(:log_output) {StringIO.new}
+  let(:logger) {
+    logger = Logger.new(log_output)
+    logger.formatter = ->(_, _, _, msg) {
+      msg
+    }
+    def log_output.json
+      JSON.parse! self.string
+    end
+    logger
+  }
+  before do
+    LogStasher.logger = logger
+    LogStasher.log_controller_parameters = true
+    LogStasher.custom_fields = []
+  end
+  after do
+    LogStasher.log_controller_parameters = false
+  end
+
+  let(:subscriber) {LogStasher::RequestLogSubscriber.new}
+  let(:event) {
+    ActiveSupport::Notifications::Event.new(
+      'process_action.action_controller', Time.now, Time.now, 2, {
+        status: 200, format: 'application/json', method: 'GET', path: '/home?foo=bar', params: {
+          :controller => 'home', :action => 'index', 'foo' => 'bar'
+        }.with_indifferent_access, db_runtime: 0.02, view_runtime: 0.01
+      }
+    )
+  }
+
+  let(:redirect) {
+    ActiveSupport::Notifications::Event.new(
+      'redirect_to.action_controller', Time.now, Time.now, 1, location: 'http://example.com', status: 302
+    )
+  }
+
+  describe '.process_action' do
+    let!(:request_subscriber) { @request_subscriber ||= LogStasher::RequestLogSubscriber.new() }
+    let(:payload) { {} }
+    let(:event)   { double(:payload => payload) }
+    let(:logger)  { double }
+    let(:json)    { "{\"@source\":\"unknown\",\"@tags\":[\"request\"],\"@fields\":{\"request\":true,\"status\":true,\"runtimes\":true,\"location\":true,\"exception\":true,\"custom\":true},\"@timestamp\":\"timestamp\"}\n" }
+    before do
+      LogStasher.stub(:logger => logger)
+      LogStash::Time.stub(:now => 'timestamp')
+    end
+    it 'calls all extractors and outputs the json' do
+      request_subscriber.should_receive(:extract_request).with(payload).and_return({:request => true})
+      request_subscriber.should_receive(:extract_status).with(payload).and_return({:status => true})
+      request_subscriber.should_receive(:runtimes).with(event).and_return({:runtimes => true})
+      request_subscriber.should_receive(:location).with(event).and_return({:location => true})
+      request_subscriber.should_receive(:extract_exception).with(payload).and_return({:exception => true})
+      request_subscriber.should_receive(:extract_custom_fields).with(payload).and_return({:custom => true})
+      LogStasher.logger.should_receive(:<<).with(json)
+      request_subscriber.process_action(event)
+    end
+  end
+
+  describe 'logstasher output' do
+
+    it "should contain request tag" do
+      subscriber.process_action(event)
+      log_output.json['@tags'].should include 'request'
+    end
+
+    it "should contain HTTP method" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['method'].should == 'GET'
+    end
+
+    it "should include the path in the log output" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['path'].should == '/home'
+    end
+
+    it "should include the format in the log output" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['format'].should == 'application/json'
+    end
+
+    it "should include the status code" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['status'].should == 200
+    end
+
+    it "should include the controller" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['controller'].should == 'home'
+    end
+
+    it "should include the action" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['action'].should == 'index'
+    end
+
+    it "should include the view rendering time" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['view'].should == 0.01
+    end
+
+    it "should include the database rendering time" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['db'].should == 0.02
+    end
+
+    it "should add a valid status when an exception occurred" do
+      begin
+        raise AbstractController::ActionNotFound.new('Could not find an action')
+      # working this in rescue to get access to $! variable
+      rescue
+        event.payload[:status] = nil
+        event.payload[:exception] = ['AbstractController::ActionNotFound', 'Route not found']
+        subscriber.process_action(event)
+        log_output.json['@fields']['status'].should >= 400
+        log_output.json['@fields']['error'].should =~ /AbstractController::ActionNotFound.*Route not found.*logstasher\/spec\/lib\/logstasher\/log_subscriber_spec\.rb/m
+        log_output.json['@tags'].should include 'request'
+        log_output.json['@tags'].should include 'exception'
+      end
+    end
+
+    it "should return an unknown status when no status or exception is found" do
+      event.payload[:status] = nil
+      event.payload[:exception] = nil
+      subscriber.process_action(event)
+      log_output.json['@fields']['status'].should == 0
+    end
+
+    describe "with a redirect" do
+      before do
+        Thread.current[:logstasher_location] = "http://www.example.com"
+      end
+
+      it "should add the location to the log line" do
+        subscriber.process_action(event)
+        log_output.json['@fields']['location'].should == 'http://www.example.com'
+      end
+
+      it "should remove the thread local variable" do
+        subscriber.process_action(event)
+        Thread.current[:logstasher_location].should == nil
+      end
+    end
+
+    it "should not include a location by default" do
+      subscriber.process_action(event)
+      log_output.json['@fields']['location'].should be_nil
+    end
+  end
+
+  describe "with append_custom_params block specified" do
+    let(:request) { double(:remote_ip => '10.0.0.1')}
+    it "should add default custom data to the output" do
+      request.stub(:params => event.payload[:params])
+      LogStasher.add_default_fields_to_payload(event.payload, request)
+      subscriber.process_action(event)
+      log_output.json['@fields']['ip'].should == '10.0.0.1'
+      log_output.json['@fields']['route'].should == 'home#index'
+      log_output.json['@fields']['parameters'].should == {'foo' => 'bar'}
+    end
+  end
+
+  describe "with append_custom_params block specified" do
+    before do
+      LogStasher.stub(:add_custom_fields) do |&block|
+        @block = block
+      end
+      LogStasher.add_custom_fields do |payload|
+        payload[:user] = 'user'
+      end
+      LogStasher.custom_fields += [:user]
+    end
+
+    it "should add the custom data to the output" do
+      @block.call(event.payload)
+      subscriber.process_action(event)
+      log_output.json['@fields']['user'].should == 'user'
+    end
+  end
+
+  describe "when processing a redirect" do
+    it "should store the location in a thread local variable" do
+      subscriber.redirect_to(redirect)
+      Thread.current[:logstasher_location].should == "http://example.com"
+    end
+  end
+end
diff --git a/ruby-logstasher/spec/lib/logstasher_spec.rb b/ruby-logstasher/spec/lib/logstasher_spec.rb
new file mode 100644 (file)
index 0000000..aee18f2
--- /dev/null
@@ -0,0 +1,179 @@
+require 'spec_helper'
+
+describe LogStasher do
+  describe "when removing Rails' log subscribers" do
+    after do
+      ActionController::LogSubscriber.attach_to :action_controller
+      ActionView::LogSubscriber.attach_to :action_view
+    end
+
+    it "should remove subscribers for controller events" do
+      expect {
+        LogStasher.remove_existing_log_subscriptions
+      }.to change {
+        ActiveSupport::Notifications.notifier.listeners_for('process_action.action_controller')
+      }
+    end
+
+    it "should remove subscribers for all events" do
+      expect {
+        LogStasher.remove_existing_log_subscriptions
+      }.to change {
+        ActiveSupport::Notifications.notifier.listeners_for('render_template.action_view')
+      }
+    end
+
+    it "shouldn't remove subscribers that aren't from Rails" do
+      blk = -> {}
+      ActiveSupport::Notifications.subscribe("process_action.action_controller", &blk)
+      LogStasher.remove_existing_log_subscriptions
+      listeners = ActiveSupport::Notifications.notifier.listeners_for('process_action.action_controller')
+      listeners.size.should > 0
+    end
+  end
+
+  describe '.appened_default_info_to_payload' do
+    let(:params)  { {'a' => '1', 'b' => 2, 'action' => 'action', 'controller' => 'test'}.with_indifferent_access }
+    let(:payload) { {:params => params} }
+    let(:request) { double(:params => params, :remote_ip => '10.0.0.1')}
+    after do
+      LogStasher.custom_fields = []
+      LogStasher.log_controller_parameters = false
+    end
+    it 'appends default parameters to payload' do
+      LogStasher.log_controller_parameters = true
+      LogStasher.custom_fields = []
+      LogStasher.add_default_fields_to_payload(payload, request)
+      payload[:ip].should == '10.0.0.1'
+      payload[:route].should == 'test#action'
+      payload[:parameters].should == {'a' => '1', 'b' => 2}
+      LogStasher.custom_fields.should == [:ip, :route, :parameters]
+    end
+
+    it 'does not include parameters when not configured to' do
+      LogStasher.custom_fields = []
+      LogStasher.add_default_fields_to_payload(payload, request)
+      payload.should_not have_key(:parameters)
+      LogStasher.custom_fields.should == [:ip, :route]
+    end
+  end
+
+  describe '.append_custom_params' do
+    let(:block) { ->{} }
+    it 'defines a method in ActionController::Base' do
+      ActionController::Base.should_receive(:send).with(:define_method, :logtasher_add_custom_fields_to_payload, &block)
+      LogStasher.add_custom_fields(&block)
+    end
+  end
+
+  shared_examples 'setup' do
+    let(:logger) { double }
+    let(:logstasher_config) { double(:logger => logger, :log_level => 'warn', :log_controller_parameters => nil, :source => logstasher_source) }
+    let(:config) { double(:logstasher => logstasher_config) }
+    let(:app) { double(:config => config) }
+    before do
+      @previous_source = LogStasher.source
+      config.stub(:action_dispatch => double(:rack_cache => false))
+    end
+    after { LogStasher.source = @previous_source } # Need to restore old source for specs
+    it 'defines a method in ActionController::Base' do
+      LogStasher.should_receive(:require).with('logstasher/rails_ext/action_controller/metal/instrumentation')
+      LogStasher.should_receive(:require).with('logstash-event')
+      LogStasher.should_receive(:suppress_app_logs).with(app)
+      LogStasher::RequestLogSubscriber.should_receive(:attach_to).with(:action_controller)
+      logger.should_receive(:level=).with('warn')
+      LogStasher.setup(app)
+      LogStasher.source.should == (logstasher_source || 'unknown')
+      LogStasher.enabled.should be_true
+      LogStasher.custom_fields.should == []
+      LogStasher.log_controller_parameters.should == false
+    end
+  end
+
+  describe '.setup' do
+    describe 'with source set' do
+      let(:logstasher_source) { 'foo' }
+      it_behaves_like 'setup'
+    end
+
+    describe 'without source set (default behaviour)' do
+      let(:logstasher_source) { nil }
+      it_behaves_like 'setup'
+    end
+  end
+
+  describe '.suppress_app_logs' do
+    let(:logstasher_config){ double(:logstasher => double(:suppress_app_log => true))}
+    let(:app){ double(:config => logstasher_config)}
+    it 'removes existing subscription if enabled' do
+      LogStasher.should_receive(:require).with('logstasher/rails_ext/rack/logger')
+      LogStasher.should_receive(:remove_existing_log_subscriptions)
+      LogStasher.suppress_app_logs(app)
+    end
+
+    context 'when disabled' do
+      let(:logstasher_config){ double(:logstasher => double(:suppress_app_log => false)) }
+      it 'does not remove existing subscription' do
+        LogStasher.should_not_receive(:remove_existing_log_subscriptions)
+        LogStasher.suppress_app_logs(app)
+      end
+
+      describe "backward compatibility" do
+        context 'with spelling "supress_app_log"' do
+          let(:logstasher_config){ double(:logstasher => double(:suppress_app_log => nil, :supress_app_log => false)) }
+          it 'does not remove existing subscription' do
+            LogStasher.should_not_receive(:remove_existing_log_subscriptions)
+            LogStasher.suppress_app_logs(app)
+          end
+        end
+      end
+    end
+  end
+
+  describe '.appended_params' do
+    it 'returns the stored var in current thread' do
+      Thread.current[:logstasher_custom_fields] = :test
+      LogStasher.custom_fields.should == :test
+    end
+  end
+
+  describe '.appended_params=' do
+    it 'returns the stored var in current thread' do
+      LogStasher.custom_fields = :test
+      Thread.current[:logstasher_custom_fields].should == :test
+    end
+  end
+
+  describe '.log' do
+    let(:logger) { double() }
+    before do
+      LogStasher.logger = logger
+      LogStash::Time.stub(:now => 'timestamp')
+    end
+    it 'adds to log with specified level' do
+      logger.should_receive(:send).with('warn?').and_return(true)
+      logger.should_receive(:send).with('warn',"{\"@source\":\"unknown\",\"@tags\":[\"log\"],\"@fields\":{\"message\":\"WARNING\",\"level\":\"warn\"},\"@timestamp\":\"timestamp\"}")
+      LogStasher.log('warn', 'WARNING')
+    end
+    context 'with a source specified' do
+      before :each do
+        LogStasher.source = 'foo'
+      end
+      it 'sets the correct source' do
+        logger.should_receive(:send).with('warn?').and_return(true)
+        logger.should_receive(:send).with('warn',"{\"@source\":\"foo\",\"@tags\":[\"log\"],\"@fields\":{\"message\":\"WARNING\",\"level\":\"warn\"},\"@timestamp\":\"timestamp\"}")
+        LogStasher.log('warn', 'WARNING')
+      end
+    end
+  end
+
+  %w( fatal error warn info debug unknown ).each do |severity|
+    describe ".#{severity}" do
+      let(:message) { "This is a #{severity} message" }
+      it 'should log with specified level' do
+        LogStasher.should_receive(:log).with(severity.to_sym, message)
+        LogStasher.send(severity, message )
+      end
+    end
+  end
+end
diff --git a/ruby-logstasher/spec/spec_helper.rb b/ruby-logstasher/spec/spec_helper.rb
new file mode 100644 (file)
index 0000000..1556eb6
--- /dev/null
@@ -0,0 +1,50 @@
+# Notice there is a .rspec file in the root folder. It defines rspec arguments
+
+# Ruby 1.9 uses simplecov. The ENV['COVERAGE'] is set when rake coverage is run in ruby 1.9
+if ENV['COVERAGE']
+  require 'simplecov'
+  SimpleCov.start do
+    # Remove the spec folder from coverage. By default all code files are included. For more config options see
+    # https://github.com/colszowka/simplecov
+    add_filter File.expand_path('../../spec', __FILE__)
+  end
+end
+
+# Modify load path so you can require 'ogstasher directly.
+$LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
+
+require 'rubygems'
+# Loads bundler setup tasks. Now if I run spec without installing gems then it would say gem not installed and
+# do bundle install instead of ugly load error on require.
+require 'bundler/setup'
+
+# This will require me all the gems automatically for the groups. If I do only .setup then I will have to require gems
+# manually. Note that you have still have to require some gems if they are part of bigger gem like ActiveRecord which is
+# part of Rails. You can say :require => false in gemfile to always use explicit requiring
+Bundler.require(:default, :test)
+
+# Set Rails environment as test
+ENV['RAILS_ENV'] = 'test'
+
+require 'action_pack'
+require 'action_controller'
+require 'logstasher'
+require 'active_support/notifications'
+require 'active_support/core_ext/string'
+require 'active_support/log_subscriber'
+require 'action_controller/log_subscriber'
+require 'action_view/log_subscriber'
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/string'
+require 'active_support/core_ext/time/zones'
+require 'abstract_controller/base'
+require 'logger'
+require 'logstash-event'
+
+RSpec.configure do |config|
+  config.treat_symbols_as_metadata_keys_with_true_values = true
+  config.run_all_when_everything_filtered = true
+  config.filter_run :focus
+end