chillout 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in chillout.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Michał Łomnicki
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # Chillout
2
+
3
+ ## Installation
4
+
5
+ Add this line to your Rails application's Gemfile:
6
+
7
+ gem 'chillout'
8
+
9
+ And then execute:
10
+
11
+ $ bundle
12
+
13
+ And configure in config/environments/production.rb:
14
+
15
+ ```ruby
16
+ config.chillout = {
17
+ secret: '<YOUR_SECRET>',
18
+ ssl: true,
19
+ port: 443,
20
+ hostname: 'api.chillout.io'
21
+ }
22
+ ```
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ task :default => :test
5
+
6
+ desc 'Run chillout unit tests.'
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'lib'
9
+ t.libs << 'test'
10
+ t.pattern = 'test/**/*_test.rb'
11
+ end
data/chillout.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'chillout/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "chillout"
8
+ gem.version = Chillout::VERSION
9
+ gem.authors = ["Michał Łomnicki", "Jan Filipowski"]
10
+ gem.email = ["michal.lomnicki@gmail.com", "jachuf@gmail.com", "dev@arkency.com"]
11
+ gem.description = "Chillout.io Ruby client"
12
+ gem.summary = "Chillout.io Ruby client"
13
+ gem.homepage = "http://chillout.io/"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency "minitest", "~>3.2.0"
21
+ gem.add_development_dependency "rake", "~> 0.9.2"
22
+ gem.add_development_dependency "mocha", "~> 0.12.7"
23
+ gem.add_development_dependency "contest", "~> 0.1.3"
24
+ gem.add_development_dependency "rack-test", "~> 0.6.2"
25
+ gem.add_development_dependency "webmock", "~> 1.8.11"
26
+ end
@@ -0,0 +1,52 @@
1
+ require 'forwardable'
2
+ require 'chillout/server_side'
3
+ require 'chillout/http_client'
4
+ require 'chillout/error_filter'
5
+ require 'chillout/dispatcher'
6
+ require 'chillout/config'
7
+ require 'chillout/error'
8
+ require 'chillout/event_data_builder'
9
+ require 'chillout/prefixed_logger'
10
+
11
+ module Chillout
12
+ class Client
13
+ extend Forwardable
14
+
15
+ def_delegators :@dispatcher, :dispatch_error, :send_error, :send_creations
16
+
17
+ attr_reader :config
18
+ attr_reader :logger
19
+
20
+ def initialize(config_or_api_key, options = {})
21
+ build_config(config_or_api_key, options)
22
+
23
+ yield @config if block_given?
24
+
25
+ @logger = PrefixedLogger.new("Chillout", @config.logger)
26
+
27
+ @http_client = HttpClient.new(@config, logger)
28
+ @event_data_builder = EventDataBuilder.new(@config)
29
+ @server_side = ServerSide.new(@event_data_builder, @http_client)
30
+ @filter = ErrorFilter.new
31
+ @dispatcher = Dispatcher.new(@filter, @server_side)
32
+ end
33
+
34
+ def send_exception(exception, environment = {})
35
+ send_error(Error.new(exception, environment))
36
+ end
37
+
38
+ private
39
+ def build_config(config_or_api_key, options)
40
+ case config_or_api_key
41
+ when Config
42
+ @config = config_or_api_key
43
+ when String
44
+ @config = Config.new(config_or_api_key)
45
+ else
46
+ raise ArgumentError.new("Invalid config passed")
47
+ end
48
+ @config.update(options)
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+ require 'chillout/version'
3
+
4
+
5
+ module Chillout
6
+ class Config
7
+
8
+ DEFAULT_HOSTNAME = "api.chillout.io"
9
+ DEFAULT_PORT = 80
10
+ DEFAULT_NOTIFIER_NAME = "Chillout"
11
+ DEFAULT_NOTIFIER_URL = "http://github.com/chilloutio/chillout"
12
+ DEFAULT_SHELL_ENV = ENV.to_hash
13
+
14
+ attr_accessor :environment
15
+ attr_accessor :shell_environment
16
+ attr_accessor :notifier_name
17
+ attr_accessor :notifier_url
18
+ attr_accessor :version
19
+ attr_accessor :platform
20
+ attr_accessor :hostname
21
+ attr_accessor :port
22
+ attr_accessor :api_key
23
+ attr_writer :authentication_user
24
+ attr_writer :authentication_password
25
+ attr_accessor :logger
26
+ attr_accessor :ssl
27
+
28
+ def initialize(api_key = nil)
29
+ @api_key = api_key
30
+ @hostname = DEFAULT_HOSTNAME
31
+ @port = DEFAULT_PORT
32
+ @notifier_name = DEFAULT_NOTIFIER_NAME
33
+ @notifier_url = DEFAULT_NOTIFIER_URL
34
+ @version = VERSION
35
+ @shell_environment = DEFAULT_SHELL_ENV
36
+ @logger = Logger.new(STDOUT)
37
+ @ssl = true
38
+ end
39
+
40
+ def update(options)
41
+ options.each do |name, value|
42
+ send("#{name}=", value)
43
+ end
44
+ end
45
+
46
+ def authentication_user
47
+ @authentication_user || api_key
48
+ end
49
+
50
+ def authentication_password
51
+ @authentication_password || api_key
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ require 'chillout/creations_container'
2
+
3
+ module Chillout
4
+ module CreationListener
5
+ def inherited(subclass)
6
+ super
7
+ class_name = subclass.name
8
+ subclass.after_commit on: :create do
9
+ Thread.current[:creations] ||= CreationsContainer.new
10
+ creations = Thread.current[:creations]
11
+ creations.increment!(class_name)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ module Chillout
2
+ class CreationsContainer
3
+ def initialize
4
+ @container = Hash.new {|hash, key| hash[key] = 0}
5
+ end
6
+
7
+ def increment!(class_name)
8
+ key = class_name.to_sym
9
+ @container[key] += 1
10
+ end
11
+
12
+ def [](name)
13
+ key = name.to_sym
14
+ @container[key]
15
+ end
16
+
17
+ def resource_keys
18
+ @container.keys
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module Chillout
2
+ class Dispatcher
3
+
4
+ def initialize(filter, server_side)
5
+ @filter = filter
6
+ @server_side = server_side
7
+ end
8
+
9
+ def dispatch_error(error)
10
+ if @filter.deliver_error?(error)
11
+ send_error(error)
12
+ end
13
+ end
14
+
15
+ def send_error(error)
16
+ @server_side.send_error(error)
17
+ rescue HttpClient::NotSent
18
+ end
19
+
20
+ def send_creations(creations)
21
+ @server_side.send_creations(creations)
22
+ rescue HttpClient::NotSent
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ require 'ostruct'
2
+
3
+ module Chillout
4
+ class Error
5
+
6
+ attr_reader :exception
7
+ attr_reader :environment
8
+
9
+ def initialize(exception, environment = {})
10
+ @exception = exception
11
+ @environment = environment
12
+ @nullObject = OpenStruct.new
13
+ end
14
+
15
+ def exception_class
16
+ exception.class.name
17
+ end
18
+
19
+ def message
20
+ exception.message
21
+ end
22
+
23
+ def backtrace
24
+ exception.backtrace
25
+ end
26
+
27
+ def file
28
+ backtrace.first.split(":").first rescue nil
29
+ end
30
+
31
+ def controller
32
+ environment['action_controller.instance'] || @nullObject
33
+ end
34
+
35
+ def controller_name
36
+ controller.controller_name
37
+ end
38
+
39
+ def controller_action
40
+ controller.action_name
41
+ end
42
+
43
+ def current_user
44
+ environment['current_user'] || @nullObject
45
+ end
46
+
47
+ def current_user_id
48
+ current_user.id if current_user.respond_to?(:id)
49
+ end
50
+
51
+ def current_user_email
52
+ current_user.email if current_user.respond_to?(:email)
53
+ end
54
+
55
+ def current_user_full_name
56
+ current_user.full_name if current_user.respond_to?(:full_name)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,9 @@
1
+ module Chillout
2
+ class ErrorFilter
3
+
4
+ def deliver_error?(error)
5
+ true
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,67 @@
1
+ module Chillout
2
+ class EventDataBuilder
3
+ def initialize(config)
4
+ @config = config
5
+ end
6
+
7
+ def build_from_error(error, timestamp)
8
+ {
9
+ event: "exception",
10
+ timestamp: timestamp,
11
+ content: {
12
+ class: error.exception_class,
13
+ message: error.message,
14
+ backtrace: error.backtrace,
15
+ file: error.file,
16
+ environment: @config.environment,
17
+ context: {
18
+ platform: @config.platform,
19
+ controller: error.controller_name,
20
+ action: error.controller_action,
21
+ current_user: {
22
+ id: error.current_user_id,
23
+ email: error.current_user_email,
24
+ full_name: error.current_user_full_name
25
+ }
26
+ },
27
+ rack_environment: build_rack_environment(error),
28
+ shell_environment: @config.shell_environment
29
+ },
30
+ notifier: build_notifier
31
+ }
32
+ end
33
+
34
+ def build_from_creations_container(creations_container, timestamp)
35
+ {
36
+ metric: "creations",
37
+ timestamp: timestamp,
38
+ content: {
39
+ creations: build_creations_content(creations_container),
40
+ environment: @config.environment
41
+ },
42
+ notifier: build_notifier
43
+ }
44
+ end
45
+
46
+ def build_creations_content(creations_container)
47
+ creation_tuples = creations_container.resource_keys.map do |key|
48
+ [key, creations_container[key]]
49
+ end
50
+ Hash[creation_tuples]
51
+ end
52
+
53
+ def build_notifier
54
+ {
55
+ name: @config.notifier_name,
56
+ version: @config.version,
57
+ url: @config.notifier_url
58
+ }
59
+ end
60
+
61
+ def build_rack_environment(error)
62
+ Hash[error.environment.collect do |key, value|
63
+ [key, value.to_s]
64
+ end]
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ require 'net/http'
2
+ require 'json'
3
+
4
+ module Chillout
5
+ class HttpClient
6
+ class NotSent < StandardError
7
+ attr_reader :original_exception
8
+ def initialize(original_exception)
9
+ @original_exception = original_exception
10
+ end
11
+ end
12
+
13
+ MEDIA_TYPE = "application/vnd.chillout.v1+json"
14
+
15
+ def initialize(config, logger)
16
+ @config = config
17
+ @logger = logger
18
+ end
19
+
20
+ def post(path, data)
21
+ http = Net::HTTP.new(@config.hostname, @config.port)
22
+ http.use_ssl = @config.ssl
23
+ request_spec = Net::HTTP::Post.new(path)
24
+ request_spec.body = JSON.dump(data)
25
+ request_spec.content_type = MEDIA_TYPE
26
+ request_spec.basic_auth @config.authentication_user, @config.authentication_password
27
+ http.start do
28
+ http.request(request_spec)
29
+ end
30
+ rescue => e
31
+ @logger.error("#{e.class}: #{e.message}")
32
+ raise NotSent.new(e)
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,19 @@
1
+ module Chillout
2
+ module Middleware
3
+ class CreationsMonitor
4
+ def initialize(app, client)
5
+ @app = app
6
+ @client = client
7
+ end
8
+
9
+ def call(env)
10
+ response = @app.call(env)
11
+ if Thread.current[:creations]
12
+ @client.send_creations(Thread.current[:creations])
13
+ Thread.current[:creations] = nil
14
+ end
15
+ response
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ module Chillout
2
+ module Middleware
3
+ class ExceptionMonitor
4
+ def initialize(app, client)
5
+ @app = app
6
+ @client = client
7
+ end
8
+
9
+ def call(env)
10
+ begin
11
+ response = @app.call(env)
12
+ rescue Exception => exception
13
+ @client.dispatch_error(Chillout::Error.new(exception, env))
14
+ raise exception
15
+ end
16
+
17
+ if env['rack.exception']
18
+ @client.dispatch_error(Chillout::Error.new(env['rack.exception'], env))
19
+ end
20
+
21
+ response
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ module Chillout
2
+ class PrefixedLogger
3
+ attr_reader :prefix
4
+
5
+ def initialize(prefix, logger)
6
+ @prefix = prefix
7
+ @logger = logger
8
+ end
9
+
10
+ [:error, :fatal, :warn, :info, :debug].each do |method_name|
11
+ define_method method_name do |message|
12
+ @logger.send(method_name, "[Chillout] #{message}")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ require 'chillout/creation_listener'
2
+
3
+ module Chillout
4
+ class Railtie < Rails::Railtie
5
+ config.chillout = ActiveSupport::OrderedOptions.new
6
+ initializer "chillout.exceptions_listener_initialization" do |app|
7
+ if !app.config.chillout.empty?
8
+ existing_option_keys = [:port, :hostname, :ssl].select{|key| app.config.chillout.has_key?(key)}
9
+ options_list = existing_option_keys.map{|key| [key, app.config.chillout[key]]}
10
+ options = Hash[options_list].merge(logger: Rails.logger)
11
+
12
+ client = Client.new(app.config.chillout[:secret], options)
13
+
14
+ ActiveRecord::Base.extend(CreationListener)
15
+
16
+ app.middleware.use Middleware::ExceptionMonitor, client
17
+ app.middleware.use Middleware::CreationsMonitor, client
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ require 'time'
2
+ require 'chillout/event_data_builder'
3
+
4
+ module Chillout
5
+ class ServerSide
6
+
7
+ def initialize(event_data_builder, http_client)
8
+ @http_client = http_client
9
+ @event_data_builder = event_data_builder
10
+ end
11
+
12
+ def send_error(error)
13
+ event_data = @event_data_builder.build_from_error(error, timestamp)
14
+ send_event(event_data)
15
+ end
16
+
17
+ def send_event(data)
18
+ @http_client.post('/events', data)
19
+ end
20
+
21
+ def send_creations(creations_container)
22
+ event_data = @event_data_builder.build_from_creations_container(creations_container, timestamp)
23
+ send_metric(event_data)
24
+ end
25
+
26
+ def send_metric(data)
27
+ @http_client.post('/metrics', data)
28
+ end
29
+
30
+ private
31
+ def timestamp
32
+ Time.now.iso8601
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Chillout
2
+ VERSION = "0.2.0"
3
+ end
data/lib/chillout.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "chillout/version"
2
+ require "chillout/config"
3
+ require "chillout/middleware/exception_monitor"
4
+ require "chillout/middleware/creations_monitor"
5
+ require "chillout/error"
6
+ require "chillout/dispatcher"
7
+ require "chillout/error_filter"
8
+ require "chillout/server_side"
9
+ require "chillout/http_client"
10
+ require "chillout/client"
11
+
12
+ module Chillout
13
+ end
14
+
15
+ require 'chillout/railtie' if defined?(Rails)
16
+
@@ -0,0 +1,40 @@
1
+ require 'test_helper'
2
+
3
+ class ClientTest < ChilloutTestCase
4
+
5
+ def test_initialize_with_config
6
+ config = Chillout::Config.new("xyz123")
7
+ client = Chillout::Client.new(config)
8
+ assert_equal config, client.config
9
+ end
10
+
11
+ def test_initialize_with_options_hash
12
+ client = Chillout::Client.new("xyz123", platform: 'rack')
13
+ assert_equal "xyz123", client.config.api_key
14
+ assert_equal "rack", client.config.platform
15
+ end
16
+
17
+ def test_initialize_with_block
18
+ client = Chillout::Client.new("xyz") do |config|
19
+ config.platform = "rack"
20
+ end
21
+ assert_equal "xyz", client.config.api_key
22
+ assert_equal "rack", client.config.platform
23
+ end
24
+
25
+ def test_initialize_with_options_hash_and_block
26
+ client = Chillout::Client.new("xyz", platform: 'rack') do |config|
27
+ config.port = 443
28
+ end
29
+ assert_equal "xyz", client.config.api_key
30
+ assert_equal "rack", client.config.platform
31
+ assert_equal 443, client.config.port
32
+ end
33
+
34
+ def test_initialize_with_unsupported_type_raises_an_error
35
+ assert_raises(ArgumentError) do
36
+ Chillout::Client.new(123)
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,26 @@
1
+ require 'test_helper'
2
+
3
+ class ConfigTest < ChilloutTestCase
4
+
5
+ def setup
6
+ @config = Chillout::Config.new("xyz123")
7
+ end
8
+
9
+ def test_api_key_is_set
10
+ assert_equal "xyz123", @config.api_key
11
+ end
12
+
13
+ def test_update_with_options_hash
14
+ @config.update(platform: 'rack')
15
+ assert_equal 'rack', @config.platform
16
+ end
17
+
18
+ def test_authentication_user_is_same_as_api_key
19
+ assert_equal @config.api_key, @config.authentication_user
20
+ end
21
+
22
+ def test_authentication_password_is_same_as_api_key
23
+ assert_equal @config.api_key, @config.authentication_password
24
+ end
25
+
26
+ end
@@ -0,0 +1,12 @@
1
+ require 'test_helper'
2
+ require 'chillout/creations_container'
3
+
4
+ module Chillout
5
+ class CreationsContainerTest < ChilloutTestCase
6
+ def test_increment_model
7
+ creations_container = CreationsContainer.new
8
+ creations_container.increment!("User")
9
+ assert_equal 1, creations_container[:User]
10
+ end
11
+ end
12
+ end