--- /dev/null
+*.gem
+.bundle
+Gemfile.lock
+pkg/*
+.idea
\ No newline at end of file
--- /dev/null
+--color
+--format progress
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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
+
--- /dev/null
+# Logstasher [](http://badge.fury.io/rb/logstasher) [](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:
+
+[](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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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'.
--- /dev/null
+# FIXME: READMEs found
+# README.md
--- /dev/null
+---
+- spec/lib/logstasher/device/redis_spec.rb
+- spec/lib/logstasher/log_subscriber_spec.rb
+- spec/lib/logstasher_spec.rb
+- spec/spec_helper.rb
--- /dev/null
+#!/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
--- /dev/null
+3.0 (quilt)
--- /dev/null
+version=3
+http://pkg-ruby-extras.alioth.debian.org/cgi-bin/gemwatch/logstasher .*/logstasher-(.*).tar.gz
--- /dev/null
+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)
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+module LogStasher
+ VERSION = "0.5.0"
+end
--- /dev/null
+# -*- 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
--- /dev/null
+--- !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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+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
--- /dev/null
+# 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