coalmine 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +11 -0
- data/README.md +65 -0
- data/Rakefile +4 -0
- data/coalmine.gemspec +25 -0
- data/lib/coalmine/capistrano.rb +33 -0
- data/lib/coalmine/configuration.rb +37 -0
- data/lib/coalmine/logger.rb +16 -0
- data/lib/coalmine/notification.rb +186 -0
- data/lib/coalmine/rack.rb +22 -0
- data/lib/coalmine/rails/action_controller.rb +15 -0
- data/lib/coalmine/rails/controller_methods.rb +57 -0
- data/lib/coalmine/rails/tasks.rb +13 -0
- data/lib/coalmine/rails.rb +26 -0
- data/lib/coalmine/railtie.rb +31 -0
- data/lib/coalmine/sender.rb +78 -0
- data/lib/coalmine/version.rb +3 -0
- data/lib/coalmine/version_notification.rb +19 -0
- data/lib/coalmine.rb +101 -0
- data/rails/init.rb +2 -0
- data/spec/coalmine/configuration_spec.rb +28 -0
- data/spec/coalmine/notification_spec.rb +115 -0
- data/spec/coalmine/sender_spec.rb +40 -0
- data/spec/coalmine_spec.rb +18 -0
- data/spec/spec_helper.rb +4 -0
- metadata +127 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# Ruby Connector for Coalmine
|
2
|
+
|
3
|
+
This connector allows you to easily send errors and log messages to the Coalmine API.
|
4
|
+
|
5
|
+
## Setup
|
6
|
+
|
7
|
+
Rails 3.x:
|
8
|
+
|
9
|
+
gem "coalmine"
|
10
|
+
|
11
|
+
## Configuration
|
12
|
+
|
13
|
+
In a Rails app create an initializer and configure as such:
|
14
|
+
|
15
|
+
Coalmine.configure do |config|
|
16
|
+
config.signature = "my_secret_signature"
|
17
|
+
config.logger = Rails.logger
|
18
|
+
end
|
19
|
+
|
20
|
+
All uncaught exceptions are automatically logged. To manually log an exception to coalmine:
|
21
|
+
|
22
|
+
begin
|
23
|
+
call_dangerous_method
|
24
|
+
rescue Exception => e
|
25
|
+
notify_coalmine(e)
|
26
|
+
end
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
To notify Coalmine of a deployment
|
31
|
+
|
32
|
+
rake coalmine:deployment[your_version,username]
|
33
|
+
|
34
|
+
# For example
|
35
|
+
rake coalmine:deployment[1.0.0,brad]
|
36
|
+
|
37
|
+
## Filtering sensitive information
|
38
|
+
|
39
|
+
Coalmine will automatically string-replace values that you deem to be sensitive and do not want to be sent out.
|
40
|
+
Coalmine automatically honors `Rails.application.config.filter_parameters`. If you wish to include additional filter properties, you can via the config:
|
41
|
+
|
42
|
+
Coalmine.configure do |config|
|
43
|
+
config.filters += ["credit-card"]
|
44
|
+
end
|
45
|
+
|
46
|
+
The above would replace all properties named `credit-card` with the value [FILTERED].
|
47
|
+
|
48
|
+
## Setting custom variables to included with notifications
|
49
|
+
|
50
|
+
You can include extra information by defining custom variables. These are automatically appended to the notification sent to Coalmine and appear in the 'Application' tab. Custom variables are added like so:
|
51
|
+
|
52
|
+
Coalmine.custom_variables[:username] = current_user.username
|
53
|
+
|
54
|
+
You will most likely initialize all your custom application variables at the beginning of the request. If you are using Rails, it might look something like:
|
55
|
+
|
56
|
+
class ApplicationController < ApplicationController::Base
|
57
|
+
before_filter :set_custom_variables
|
58
|
+
|
59
|
+
protected
|
60
|
+
|
61
|
+
def set_custom_variables
|
62
|
+
Coalmine.custom_variables[:user_name] = current_user.username
|
63
|
+
Coalmine.custom_variables[:something] = "another custom value"
|
64
|
+
end
|
65
|
+
end
|
data/Rakefile
ADDED
data/coalmine.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "coalmine/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "coalmine"
|
7
|
+
s.version = Coalmine::VERSION
|
8
|
+
s.authors = ["Brad Seefeld", "Matt Ratzloff"]
|
9
|
+
s.email = ["admin@coalmineapp.com"]
|
10
|
+
s.homepage = "https://github.com/coalmine/coalmine_ruby"
|
11
|
+
s.summary = "Coalmine Connector Ruby implementation"
|
12
|
+
s.description = "Send errors to the Coalmine API for logging and analytics."
|
13
|
+
|
14
|
+
s.rubyforge_project = "coalmine"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "fakeweb"
|
24
|
+
s.add_runtime_dependency "jsonbuilder"
|
25
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "capistrano"
|
2
|
+
|
3
|
+
module Coalmine
|
4
|
+
module Capistrano
|
5
|
+
def self.setup(config)
|
6
|
+
config.load do
|
7
|
+
after "deploy", "coalmine:deploy"
|
8
|
+
|
9
|
+
namespace :coalmine do
|
10
|
+
desc "Notify Coalmine of the deployment"
|
11
|
+
task :deploy, :except => { :no_release => true } do
|
12
|
+
environment = fetch(:rails_env, :production)
|
13
|
+
author = ENV["USER"] || ENV["USERNAME"]
|
14
|
+
rake = fetch(:rake, :rake)
|
15
|
+
cmd = "cd #{config.current_release} && RAILS_ENV=#{environment} #{rake} coalmine:deployment[#{current_revision},#{author}]"
|
16
|
+
logger.info "Notifying Coalmine of Deploy"
|
17
|
+
if config.dry_run
|
18
|
+
logger.info "Dry Run... Coalmine will not actually be notified"
|
19
|
+
else
|
20
|
+
run(cmd, :once => true) { |ch, stream, data| result << data }
|
21
|
+
end
|
22
|
+
|
23
|
+
logger.info "Coalmine notification completed."
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
if Capistrano::Configuration.instance
|
32
|
+
Coalmine::Capistrano.setup(Capistrano::Configuration.instance)
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Coalmine::Configuration
|
2
|
+
|
3
|
+
attr_accessor :url, :environment, :signature, :logger, :host, :port, :proxy_host,
|
4
|
+
:proxy_port, :proxy_user, :proxy_password, :protocol, :secure, :http_open_timeout,
|
5
|
+
:http_read_timeout, :project_root, :framework, :filters,
|
6
|
+
|
7
|
+
# For HTTP basic auth
|
8
|
+
:http_user, :http_password,
|
9
|
+
|
10
|
+
# The application version
|
11
|
+
:version,
|
12
|
+
|
13
|
+
# The environments for which notifications can be posted
|
14
|
+
:enabled_environments
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.protocol = "https"
|
18
|
+
self.host = "coalmineapp.com"
|
19
|
+
self.port = 443
|
20
|
+
self.secure = true
|
21
|
+
self.enabled_environments = ["production", "staging"]
|
22
|
+
|
23
|
+
self.http_open_timeout = 3
|
24
|
+
self.http_read_timeout = 6
|
25
|
+
self.logger = Coalmine::Logger.new
|
26
|
+
self.filters = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def protocol=(proto)
|
30
|
+
proto = "http" unless ["http", "https"].include?(proto)
|
31
|
+
@protocol = proto
|
32
|
+
end
|
33
|
+
|
34
|
+
def secure?
|
35
|
+
self.secure
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
require "jsonbuilder"
|
2
|
+
require "socket"
|
3
|
+
|
4
|
+
##
|
5
|
+
# Contains all data sent to the API. Responsible for serializing data into the
|
6
|
+
# correct format.
|
7
|
+
#
|
8
|
+
# @link http://rack.rubyforge.org/doc/files/SPEC.html
|
9
|
+
module Coalmine
|
10
|
+
class Notification
|
11
|
+
|
12
|
+
attr_accessor :stack_trace, :message, :line_number, :url,
|
13
|
+
:error_class, :controller, :action, :method, :parameters,
|
14
|
+
:ip_address, :user_agent, :cookies, :environment, :server,
|
15
|
+
:severity, :hostname, :process_id, :file, :referrer, :thread_id
|
16
|
+
|
17
|
+
def initialize(args = {})
|
18
|
+
|
19
|
+
exception = args[:exception]
|
20
|
+
|
21
|
+
if exception
|
22
|
+
self.stack_trace = exception.backtrace * "\n" if exception.backtrace
|
23
|
+
self.message = exception.message
|
24
|
+
self.error_class = exception.class.name
|
25
|
+
self.line_number = extract_line_number(exception.backtrace)
|
26
|
+
self.file = extract_file_name(exception.backtrace)
|
27
|
+
end
|
28
|
+
|
29
|
+
if args[:rack_env]
|
30
|
+
set_from_rack_env(args[:rack_env])
|
31
|
+
end
|
32
|
+
|
33
|
+
args.keys.each do |key|
|
34
|
+
setter = :"#{key}="
|
35
|
+
send(setter, args[key]) if respond_to?(setter)
|
36
|
+
end
|
37
|
+
|
38
|
+
self.severity = "ERROR" unless self.severity
|
39
|
+
self.hostname = Socket.gethostname
|
40
|
+
|
41
|
+
begin
|
42
|
+
self.process_id = Process::pid
|
43
|
+
rescue
|
44
|
+
# Ignore
|
45
|
+
end
|
46
|
+
|
47
|
+
begin
|
48
|
+
self.thread_id = Thread.current.object_id
|
49
|
+
rescue
|
50
|
+
# Ignore
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def post_data
|
55
|
+
{:signature => Coalmine.config.signature, :json => to_json}
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_json(options = {})
|
59
|
+
ActiveSupport::JSON.encode(serialize(options))
|
60
|
+
end
|
61
|
+
|
62
|
+
def serialize(options)
|
63
|
+
config = Coalmine.config
|
64
|
+
results = {
|
65
|
+
:version => config.version,
|
66
|
+
:app_environment => config.environment,
|
67
|
+
:url => url,
|
68
|
+
:file => file,
|
69
|
+
:line_number => line_number,
|
70
|
+
:message => message,
|
71
|
+
:stack_trace => stack_trace,
|
72
|
+
:class => error_class,
|
73
|
+
:framework => config.framework,
|
74
|
+
:parameters => parameters,
|
75
|
+
:ip_address => ip_address,
|
76
|
+
:user_agent => user_agent,
|
77
|
+
:cookies => cookies,
|
78
|
+
:method => method,
|
79
|
+
:environment => environment,
|
80
|
+
:server => server,
|
81
|
+
:severity => severity,
|
82
|
+
:hostname => hostname,
|
83
|
+
:process_id => process_id,
|
84
|
+
:thread_id => thread_id.to_s, # Because it is a long
|
85
|
+
:referrer => referrer,
|
86
|
+
:application => Coalmine.custom_variables
|
87
|
+
}
|
88
|
+
|
89
|
+
Coalmine.filter(results)
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Remote resource path
|
94
|
+
#
|
95
|
+
# @return [String] The path of the remote resource. Will be appended to the remote base URL.
|
96
|
+
def resource_path
|
97
|
+
"/notify/"
|
98
|
+
end
|
99
|
+
|
100
|
+
protected
|
101
|
+
|
102
|
+
def set_from_rack_env(env)
|
103
|
+
return unless env
|
104
|
+
|
105
|
+
self.url = assemble_url(env)
|
106
|
+
self.ip_address = env["REMOTE_ADDR"]
|
107
|
+
self.user_agent = env["HTTP_USER_AGENT"]
|
108
|
+
self.method = env["REQUEST_METHOD"]
|
109
|
+
self.cookies = env["HTTP_COOKIE"]
|
110
|
+
self.parameters = env["QUERY_STRING"]
|
111
|
+
|
112
|
+
begin
|
113
|
+
require "rack/request"
|
114
|
+
request = ::Rack::Request.new(env) # Always returns the same request object
|
115
|
+
self.referrer = request.referrer if request.respond_to? :referrer
|
116
|
+
rescue
|
117
|
+
# Ignore
|
118
|
+
end
|
119
|
+
|
120
|
+
environment_keys = ["GATEWAY_INTERFACE", "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR",
|
121
|
+
"REMOTE_HOST", "REQUEST_METHOD", "REQUEST_URI", "SCRIPT_NAME", "SERVER_NAME",
|
122
|
+
"SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE", "HTTP_HOST", "HTTP_CONNECTION",
|
123
|
+
"HTTP_USER_AGENT", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE",
|
124
|
+
"HTTP_ACCEPT_CHARSET", "HTTP_COOKIE"]
|
125
|
+
self.environment = {}
|
126
|
+
environment_keys.each do |key|
|
127
|
+
self.environment[key] = env[key].to_s
|
128
|
+
end
|
129
|
+
|
130
|
+
server_keys = ["HTTP_CACHE_CONTROL", "rack.version", "rack.multithread", "rack.multiprocess",
|
131
|
+
"rack.run_once", "rack.url_scheme", "HTTP_VERSION", "REQUEST_PATH", "action_dispatch.secret_token",
|
132
|
+
"action_dispatch.show_exceptions", "action_dispatch.remote_ip", "rack.session",
|
133
|
+
"rack.session_options", "rack.request.cookie_hash", "action_dispatch.request.unsigned_session_cookie",
|
134
|
+
"action_dispatch.request.path_parameters"]
|
135
|
+
self.server = {}
|
136
|
+
server_keys.each do |key|
|
137
|
+
self.server[key] = env[key].to_s
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def assemble_url(env)
|
142
|
+
protocol = env["rack.url_scheme"]
|
143
|
+
protocol ||= "http"
|
144
|
+
if env["HTTP_HOST"]
|
145
|
+
host = env["HTTP_HOST"]
|
146
|
+
else
|
147
|
+
host = env["SERVER_NAME"]
|
148
|
+
unless [80, 443].include?(env["SERVER_PORT"])
|
149
|
+
host << ":#{env["SERVER_PORT"]}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
path = env["SCRIPT_NAME"] + env["PATH_INFO"]
|
153
|
+
"#{protocol}://#{host}#{path}"
|
154
|
+
end
|
155
|
+
|
156
|
+
##
|
157
|
+
# Extract the file name from a backtrace. Format for the first line is something like:
|
158
|
+
# "/Users/user/workspace/coalmine_ruby_test/app/controllers/index_controller.rb:4:in `index'"
|
159
|
+
#
|
160
|
+
# @param <String> backtrace The exceptions backtrace
|
161
|
+
# @return <String|Nil> The file name that generated the exception. This is based from the project root.
|
162
|
+
def extract_file_name(backtrace)
|
163
|
+
return unless backtrace
|
164
|
+
backtrace = backtrace.to_a
|
165
|
+
return unless backtrace.length >= 1
|
166
|
+
|
167
|
+
m = backtrace.first.match(/^(.+?):/)
|
168
|
+
return unless m and m.length > 1
|
169
|
+
m[1].gsub(Dir.getwd, "")
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Extract the line number which generated the exception.
|
174
|
+
#
|
175
|
+
# @param <String> backtrace Backtrace of the exception
|
176
|
+
# @return <Integer|Nil> The line number of the error. Nil if cannot be found.
|
177
|
+
def extract_line_number(backtrace)
|
178
|
+
return unless backtrace
|
179
|
+
backtrace = backtrace.to_a
|
180
|
+
return unless backtrace.length > 1
|
181
|
+
m = backtrace.first.match(/^.+:(\d+):/)
|
182
|
+
return unless m and m.length > 1
|
183
|
+
m[1]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Coalmine
|
2
|
+
class Rack
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
begin
|
9
|
+
response = @app.call(env)
|
10
|
+
rescue Exception => e
|
11
|
+
Coalmine.notify(e, :rack_env => env)
|
12
|
+
raise
|
13
|
+
end
|
14
|
+
|
15
|
+
if env["rack.exception"]
|
16
|
+
Coalmine.notify(env["rack.exception"], :rack_env => env)
|
17
|
+
end
|
18
|
+
|
19
|
+
response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Coalmine
|
2
|
+
module Rails
|
3
|
+
module ActionController
|
4
|
+
|
5
|
+
##
|
6
|
+
# Setup some rails magic so that when an exception is raised, it is
|
7
|
+
# first sent to our handler.
|
8
|
+
def self.included(base)
|
9
|
+
base.send(:before_filter, :clear_coalmine_custom_variables)
|
10
|
+
base.send(:alias_method, :rescue_action_in_public_without_coalmine, :rescue_action_in_public)
|
11
|
+
base.send(:alias_method, :rescue_action_in_public, :rescue_action_in_public_with_coalmine)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Coalmine
|
2
|
+
module Rails
|
3
|
+
module ControllerMethods
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
##
|
8
|
+
# Convenience method for other developers to send information to coalmine.
|
9
|
+
#
|
10
|
+
# @param [Hash|Exception] The exception or hash to send to Coalmine API.
|
11
|
+
def notify_coalmine(exception_or_hash)
|
12
|
+
return unless coalmine_notification_allowed?
|
13
|
+
Coalmine.notify(exception_or_hash, coalmine_request_data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def clear_coalmine_custom_variables
|
17
|
+
Coalmine.custom_variables.clear
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
##
|
23
|
+
# Automatically called by the rails stack when an uncaught exception
|
24
|
+
# is encountered.
|
25
|
+
#
|
26
|
+
# @param [Exception] The uncaught exception
|
27
|
+
def rescue_action_in_public_with_coalmine(exception)
|
28
|
+
Coalmine.logger.debug "Coalmine is handling uncaught exception: #{exception.message}"
|
29
|
+
notify_coalmine(exception)
|
30
|
+
rescue_action_in_public_without_coalmine(exception)
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Gather environment information about the current request.
|
35
|
+
#
|
36
|
+
# @return [Hash] Information about the current request.
|
37
|
+
def coalmine_request_data
|
38
|
+
{
|
39
|
+
:controller => params[:controller],
|
40
|
+
:action => params[:action],
|
41
|
+
:url => request.url,
|
42
|
+
:method => request.method,
|
43
|
+
:parameters => params,
|
44
|
+
:ip_address => request.remote_ip
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
##
|
49
|
+
# Determine if the we can notify coalmine.
|
50
|
+
#
|
51
|
+
# @return <Boolean> True if we can send notifications in the current env.
|
52
|
+
def coalmine_notification_allowed?
|
53
|
+
Coalmine.config.enabled_environments.include?(::Rails.env)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace :coalmine do
|
2
|
+
desc "Notify Coalmine of a recent deployment"
|
3
|
+
task :deployment, [:version, :author] => [:environment] do |t, args|
|
4
|
+
version = Coalmine::VersionNotification.new
|
5
|
+
version.version = args[:version]
|
6
|
+
version.author = args[:author]
|
7
|
+
if Coalmine::Sender.send(version)
|
8
|
+
puts "Successfully notified Coalmine of version #{args[:version]}"
|
9
|
+
else
|
10
|
+
puts "There was an error while notifying Coalmine. Please check the logs for details."
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "coalmine"
|
2
|
+
require "coalmine/rails/action_controller"
|
3
|
+
|
4
|
+
##
|
5
|
+
# Enhance rails 2 apps with coalmine error catching. This injects some methods into
|
6
|
+
# ActionController::Base which catches exceptions.
|
7
|
+
module Coalmine
|
8
|
+
module Rails
|
9
|
+
def self.init
|
10
|
+
|
11
|
+
if defined? ::ActionController::Base
|
12
|
+
Rails.logger.debug "Enhancing ActionController::Base with Coalmine goodies."
|
13
|
+
::ActionController::Base.send(:include, Coalmine::Rails::ActionController)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Add to configuration
|
17
|
+
Coalmine.configure do |config|
|
18
|
+
config.logger = Rails.logger
|
19
|
+
config.environment = Rails.env
|
20
|
+
config.project_root = Rails.root
|
21
|
+
config.framework = "Rails: #{::Rails::VERSION::STRING}" if defined? ::RAILS::VERSION
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require "coalmine"
|
2
|
+
require "rails"
|
3
|
+
|
4
|
+
module Coalmine
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
|
7
|
+
initializer "coalmine.use_rack_middleware" do |app|
|
8
|
+
app.config.middleware.use "Coalmine::Rack"
|
9
|
+
end
|
10
|
+
|
11
|
+
rake_tasks do
|
12
|
+
load "coalmine/rails/tasks.rb"
|
13
|
+
end
|
14
|
+
|
15
|
+
config.after_initialize do
|
16
|
+
# Add to configuration
|
17
|
+
Coalmine.configure do |config|
|
18
|
+
config.logger = Rails.logger
|
19
|
+
config.environment ||= Rails.env
|
20
|
+
config.project_root ||= Rails.root
|
21
|
+
config.framework ||= "Rails: #{::Rails::VERSION::STRING}" if defined? ::RAILS::VERSION
|
22
|
+
config.filters += Rails.application.config.filter_parameters
|
23
|
+
end
|
24
|
+
|
25
|
+
if defined? ::ActionController::Base
|
26
|
+
require "coalmine/rails/controller_methods"
|
27
|
+
::ActionController::Base.send(:include, Coalmine::Rails::ControllerMethods)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "net/https"
|
3
|
+
require "cgi"
|
4
|
+
|
5
|
+
module Coalmine
|
6
|
+
class Sender
|
7
|
+
|
8
|
+
HEADERS = {
|
9
|
+
"Content-type" => "application/x-www-form-urlencoded",
|
10
|
+
"Accept" => "text/json, application/json"
|
11
|
+
}
|
12
|
+
|
13
|
+
HTTP_ERRORS = [Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
14
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
|
15
|
+
Errno::ECONNREFUSED, Errno::ETIMEDOUT].freeze
|
16
|
+
|
17
|
+
def self.send(notification)
|
18
|
+
unless config.enabled_environments.include?(config.environment)
|
19
|
+
Coalmine.logger.debug "Attempted to send a notification to Coalmine, " +
|
20
|
+
"but notifications are not enabled for #{config.environment}. To " +
|
21
|
+
"send requests for this environment, add it to config.enabled_environments."
|
22
|
+
return
|
23
|
+
end
|
24
|
+
|
25
|
+
proxy = Net::HTTP::Proxy(config.proxy_host, config.proxy_port, config.proxy_user, config.proxy_password)
|
26
|
+
url = self.url(notification)
|
27
|
+
http = proxy.new(url.host, url.port)
|
28
|
+
|
29
|
+
http.read_timeout = config.http_read_timeout
|
30
|
+
http.open_timeout = config.http_open_timeout
|
31
|
+
|
32
|
+
if config.secure?
|
33
|
+
http.use_ssl = true
|
34
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
35
|
+
http.ca_file = OpenSSL::X509::DEFAULT_CERT_FILE if File.exist?(OpenSSL::X509::DEFAULT_CERT_FILE)
|
36
|
+
else
|
37
|
+
http.use_ssl = false
|
38
|
+
end
|
39
|
+
|
40
|
+
response = nil
|
41
|
+
begin
|
42
|
+
req = Net::HTTP::Post.new(url.path)
|
43
|
+
req.set_form_data(notification.post_data)
|
44
|
+
HEADERS.each_pair do |key, value|
|
45
|
+
req[key] = value
|
46
|
+
end
|
47
|
+
|
48
|
+
if config.http_user && config.http_password
|
49
|
+
req.basic_auth config.http_user, config.http_password
|
50
|
+
end
|
51
|
+
|
52
|
+
response = http.request(req)
|
53
|
+
|
54
|
+
unless response.code == "200"
|
55
|
+
Coalmine.logger.error "Unable to notify Coalmine (HTTP #{response.code})"
|
56
|
+
Coalmine.logger.error "Coalmine response: #{response.body}" if response.body
|
57
|
+
return false
|
58
|
+
end
|
59
|
+
rescue *HTTP_ERRORS => e
|
60
|
+
Coalmine.logger.error "Timeout while attempting to notify Coalmine at #{url}"
|
61
|
+
return false
|
62
|
+
end
|
63
|
+
|
64
|
+
Coalmine.logger.debug "Sent notification to Coalmine at #{url}"
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def self.url(data)
|
71
|
+
URI.parse("#{config.protocol}://#{config.host}:#{config.port}#{data.resource_path}")
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.config
|
75
|
+
Coalmine.config
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Coalmine
|
2
|
+
class VersionNotification
|
3
|
+
|
4
|
+
attr_accessor :environment, :version, :author
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.environment = Coalmine.config.environment
|
8
|
+
end
|
9
|
+
|
10
|
+
def resource_path
|
11
|
+
"/versions/"
|
12
|
+
end
|
13
|
+
|
14
|
+
def post_data
|
15
|
+
{:signature => Coalmine.config.signature, :environment => self.environment,
|
16
|
+
:version => self.version, :author => self.author}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/coalmine.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require "coalmine/configuration"
|
2
|
+
require "coalmine/sender"
|
3
|
+
require "coalmine/notification"
|
4
|
+
require "coalmine/rack"
|
5
|
+
require "coalmine/logger"
|
6
|
+
require "coalmine/version_notification"
|
7
|
+
require "coalmine/railtie" if defined? Rails
|
8
|
+
|
9
|
+
module Coalmine
|
10
|
+
|
11
|
+
##
|
12
|
+
# Configure the coalmine client.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# Coalmine.configure do |config|
|
16
|
+
# config.signature = "abc"
|
17
|
+
# config.logger = Rails.logger
|
18
|
+
# end
|
19
|
+
def self.configure
|
20
|
+
yield(config)
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Fetch the config.
|
25
|
+
#
|
26
|
+
# @return [Coalmine::Configuration] The configuration
|
27
|
+
def self.config
|
28
|
+
@configuration ||= Configuration.new
|
29
|
+
@configuration
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.logger
|
33
|
+
config.logger
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Send an exception manually to the API. This method packages up the exception and
|
38
|
+
# then sends it.
|
39
|
+
#
|
40
|
+
# @param [Exception] exception The exception to log
|
41
|
+
# @return [Boolean] True if notification is sent OK
|
42
|
+
def self.notify(exception, additional_data = {})
|
43
|
+
|
44
|
+
# We also log the exception locally.
|
45
|
+
logger.error exception
|
46
|
+
|
47
|
+
# Be paranoid about causing exceptions.
|
48
|
+
begin
|
49
|
+
notification = build_from_exception(exception, additional_data)
|
50
|
+
|
51
|
+
# Send the exception to the remote.
|
52
|
+
return send(notification)
|
53
|
+
rescue Exception => e
|
54
|
+
logger.error e
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.err(message)
|
60
|
+
notify(nil, :message => message, :severity => "ERROR")
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.error(message)
|
64
|
+
err(message)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.warn(message)
|
68
|
+
notify(nil, :message => message, :severity => "WARN")
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.info(message)
|
72
|
+
notify(nil, :message => message, :severity => "INFO")
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.debug(message)
|
76
|
+
notify(nil, :message => message, :severity => "DEBUG")
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.filter(hash)
|
80
|
+
@filter ||= ActionDispatch::Http::ParameterFilter.new(config.filters)
|
81
|
+
@filter.filter(hash)
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Variables that may be set by the application.
|
86
|
+
def self.custom_variables
|
87
|
+
@custom_variables ||= {}
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
def self.build_from_exception(exception, additional_data = {})
|
93
|
+
additional_data ||= {}
|
94
|
+
additional_data[:exception] = exception
|
95
|
+
Notification.new(additional_data)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.send(data)
|
99
|
+
Coalmine::Sender.send(data)
|
100
|
+
end
|
101
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Coalmine::Configuration do
|
4
|
+
|
5
|
+
it "defaults to http protocol" do
|
6
|
+
Coalmine::Configuration.new.protocol.should == "http"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "falls back to http when an unsupported protocol is given" do
|
10
|
+
config = Coalmine::Configuration.new
|
11
|
+
config.protocol = "something"
|
12
|
+
config.protocol.should == "http"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "accepts https as the protocol" do
|
16
|
+
config = Coalmine::Configuration.new
|
17
|
+
config.protocol = "https"
|
18
|
+
config.protocol.should == "https"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "sets appropriate timeout defaults" do
|
22
|
+
config = Coalmine::Configuration.new
|
23
|
+
config.http_read_timeout.should_not be_nil
|
24
|
+
config.http_read_timeout.should > 0
|
25
|
+
config.http_open_timeout.should_not be_nil
|
26
|
+
config.http_open_timeout.should > 0
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Coalmine::Notification do
|
4
|
+
|
5
|
+
it "grabs info from the exception" do
|
6
|
+
exception = Exception.new("a test exception")
|
7
|
+
notification = Coalmine::Notification.new(:exception => exception)
|
8
|
+
notification.message.should == exception.message
|
9
|
+
notification.stack_trace.should == exception.backtrace
|
10
|
+
notification.error_class.should == exception.class.name
|
11
|
+
end
|
12
|
+
|
13
|
+
it "serializes to json" do
|
14
|
+
exception = Exception.new("just a test")
|
15
|
+
notification = Coalmine::Notification.new(:exception => exception, :url => "http://www.test.com", :user_agent => %Q[something "with" quote's])
|
16
|
+
json = ActiveSupport::JSON.decode(notification.to_json)
|
17
|
+
|
18
|
+
required_fields = ["signature", "version", "app_environment", "url"]
|
19
|
+
required_fields.each do |field|
|
20
|
+
json.keys.include?(field)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "sets the file name" do
|
25
|
+
begin
|
26
|
+
raise Exception.new("test")
|
27
|
+
rescue Exception => e
|
28
|
+
notification = Coalmine::Notification.new(:exception => e)
|
29
|
+
notification.file.should_not be_nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "sets the line number" do
|
34
|
+
begin
|
35
|
+
raise Exception.new("test")
|
36
|
+
rescue Exception => e
|
37
|
+
notification = Coalmine::Notification.new(:exception => e)
|
38
|
+
notification.line_number.should_not be_nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "populates from a rack env" do
|
43
|
+
url = "http://localhost:3000/"
|
44
|
+
ip = "123.3.32.234"
|
45
|
+
method = "GET"
|
46
|
+
params = "key=value&some_other_key=*another(val)"
|
47
|
+
user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/13.0.782.220 Safari/535.1"
|
48
|
+
cookies = "_coalmine_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTZiZTE3NWZiNDMzNWE4NTZiNGUyOTllODkzYzQwMDk5BjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMUJaaE1pWWdnWTkwalg3ZmZTWVZKbE5GU3d4UzhJcEtJWTF2cGxmanViWEk9BjsARg%3D%3D--7ef48133c020462203c902444fe11fa8241f9c02"
|
49
|
+
|
50
|
+
rack_env = {"GATEWAY_INTERFACE" => "CGI/1.1", "PATH_INFO" => "/", "QUERY_STRING" => params,
|
51
|
+
"REMOTE_ADDR" => ip, "REMOTE_HOST" => "localhost", "REQUEST_METHOD" => method,
|
52
|
+
"REQUEST_URI" => url, "SCRIPT_NAME"=>"", "SERVER_NAME"=>"localhost", "SERVER_PORT"=>"3000",
|
53
|
+
"SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)",
|
54
|
+
"HTTP_HOST"=>"localhost:3000", "HTTP_CONNECTION"=>"keep-alive",
|
55
|
+
"HTTP_USER_AGENT"=> user_agent, "HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
56
|
+
"HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch", "HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8",
|
57
|
+
"HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.3",
|
58
|
+
"HTTP_COOKIE"=> cookies, "rack.version"=>[1, 1]}
|
59
|
+
|
60
|
+
notification = Coalmine::Notification.new(:rack_env => rack_env)
|
61
|
+
notification.url.should == url
|
62
|
+
notification.ip_address.should == ip
|
63
|
+
notification.method.should == method
|
64
|
+
notification.user_agent.should == user_agent
|
65
|
+
notification.cookies.should == cookies
|
66
|
+
notification.parameters.should == params
|
67
|
+
notification.environment.length.should >= 20
|
68
|
+
end
|
69
|
+
|
70
|
+
it "defaults to error severity" do
|
71
|
+
notification = Coalmine::Notification.new
|
72
|
+
notification.severity.should == "ERROR"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "allows severity to be set explicitly" do
|
76
|
+
severity = "INFO"
|
77
|
+
notification = Coalmine::Notification.new(:severity => severity)
|
78
|
+
notification.severity.should == severity
|
79
|
+
end
|
80
|
+
|
81
|
+
it "sets the hostname" do
|
82
|
+
notification = Coalmine::Notification.new
|
83
|
+
notification.hostname.should_not be_nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it "does not allow hostname to be set explicitly" do
|
87
|
+
hostname = "not a real hostname"
|
88
|
+
notification = Coalmine::Notification.new(:hostname => hostname)
|
89
|
+
notification.hostname.should_not == hostname
|
90
|
+
end
|
91
|
+
|
92
|
+
it "sets the process ID" do
|
93
|
+
notification = Coalmine::Notification.new
|
94
|
+
notification.process_id.should_not be_nil
|
95
|
+
end
|
96
|
+
|
97
|
+
it "does not allow the process ID to be set explicitly" do
|
98
|
+
pid = 1
|
99
|
+
notification = Coalmine::Notification.new(:process_id => pid)
|
100
|
+
notification.process_id.should_not == pid
|
101
|
+
end
|
102
|
+
|
103
|
+
it "sets the thread ID" do
|
104
|
+
notification = Coalmine::Notification.new
|
105
|
+
notification.thread_id.should_not be_nil
|
106
|
+
notification.thread_id.should > 0
|
107
|
+
end
|
108
|
+
|
109
|
+
it "does not allow the thread ID to be set explicitly" do
|
110
|
+
thread_id = 1
|
111
|
+
notification = Coalmine::Notification.new(:thread_id => thread_id)
|
112
|
+
notification.thread_id.should_not == thread_id
|
113
|
+
notification.thread_id.should_not be_nil
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Coalmine::Sender do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
FakeWeb.clean_registry
|
7
|
+
Coalmine.configure do |config|
|
8
|
+
config.signature = "test"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context "sending data" do
|
13
|
+
it "successfully sends data given the happy path" do
|
14
|
+
FakeWeb.register_uri(:post, Coalmine::Sender.url, :status => "200")
|
15
|
+
Coalmine::Sender.send("some test data").should be_true
|
16
|
+
end
|
17
|
+
|
18
|
+
it "successfully sends data through a proxy" do
|
19
|
+
Coalmine.configure do |config|
|
20
|
+
config.proxy_host = "coalmine-proxy.net"
|
21
|
+
config.proxy_port = 999
|
22
|
+
config.proxy_user = "some_user"
|
23
|
+
config.proxy_password = "Sp3c!@lCh4r#"
|
24
|
+
end
|
25
|
+
|
26
|
+
FakeWeb.register_uri(:post, Coalmine::Sender.url, :status => "200")
|
27
|
+
Coalmine::Sender.send("something").should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "fails when response code is bad" do
|
31
|
+
FakeWeb.register_uri(:post, Coalmine::Sender.url, :status => ["500", "Internal Server Error"])
|
32
|
+
Coalmine::Sender.send("test data").should be_false
|
33
|
+
end
|
34
|
+
|
35
|
+
it "catches network errors" do
|
36
|
+
Net::HTTP.any_instance.stub(:post).and_raise(Timeout::Error)
|
37
|
+
Coalmine::Sender.send("test").should be_false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Coalmine do
|
4
|
+
|
5
|
+
context "configuring" do
|
6
|
+
it "allows block style configuration" do
|
7
|
+
host = "http://www.test.com"
|
8
|
+
port = 3000
|
9
|
+
Coalmine.configure do |config|
|
10
|
+
config.host = host
|
11
|
+
config.port = port
|
12
|
+
end
|
13
|
+
|
14
|
+
Coalmine.config.host.should == host
|
15
|
+
Coalmine.config.port.should == port
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: coalmine
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
- 0
|
9
|
+
version: 0.5.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Brad Seefeld
|
13
|
+
- Matt Ratzloff
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-04-09 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rspec
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: fakeweb
|
35
|
+
prerelease: false
|
36
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
segments:
|
41
|
+
- 0
|
42
|
+
version: "0"
|
43
|
+
type: :development
|
44
|
+
version_requirements: *id002
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: jsonbuilder
|
47
|
+
prerelease: false
|
48
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
type: :runtime
|
56
|
+
version_requirements: *id003
|
57
|
+
description: Send errors to the Coalmine API for logging and analytics.
|
58
|
+
email:
|
59
|
+
- admin@coalmineapp.com
|
60
|
+
executables: []
|
61
|
+
|
62
|
+
extensions: []
|
63
|
+
|
64
|
+
extra_rdoc_files: []
|
65
|
+
|
66
|
+
files:
|
67
|
+
- .gitignore
|
68
|
+
- Gemfile
|
69
|
+
- README.md
|
70
|
+
- Rakefile
|
71
|
+
- coalmine.gemspec
|
72
|
+
- lib/coalmine.rb
|
73
|
+
- lib/coalmine/capistrano.rb
|
74
|
+
- lib/coalmine/configuration.rb
|
75
|
+
- lib/coalmine/logger.rb
|
76
|
+
- lib/coalmine/notification.rb
|
77
|
+
- lib/coalmine/rack.rb
|
78
|
+
- lib/coalmine/rails.rb
|
79
|
+
- lib/coalmine/rails/action_controller.rb
|
80
|
+
- lib/coalmine/rails/controller_methods.rb
|
81
|
+
- lib/coalmine/rails/tasks.rb
|
82
|
+
- lib/coalmine/railtie.rb
|
83
|
+
- lib/coalmine/sender.rb
|
84
|
+
- lib/coalmine/version.rb
|
85
|
+
- lib/coalmine/version_notification.rb
|
86
|
+
- rails/init.rb
|
87
|
+
- spec/coalmine/configuration_spec.rb
|
88
|
+
- spec/coalmine/notification_spec.rb
|
89
|
+
- spec/coalmine/sender_spec.rb
|
90
|
+
- spec/coalmine_spec.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
has_rdoc: true
|
93
|
+
homepage: https://github.com/coalmine/coalmine_ruby
|
94
|
+
licenses: []
|
95
|
+
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
segments:
|
106
|
+
- 0
|
107
|
+
version: "0"
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
version: "0"
|
115
|
+
requirements: []
|
116
|
+
|
117
|
+
rubyforge_project: coalmine
|
118
|
+
rubygems_version: 1.3.6
|
119
|
+
signing_key:
|
120
|
+
specification_version: 3
|
121
|
+
summary: Coalmine Connector Ruby implementation
|
122
|
+
test_files:
|
123
|
+
- spec/coalmine/configuration_spec.rb
|
124
|
+
- spec/coalmine/notification_spec.rb
|
125
|
+
- spec/coalmine/sender_spec.rb
|
126
|
+
- spec/coalmine_spec.rb
|
127
|
+
- spec/spec_helper.rb
|