macmillan-utils 1.0.11

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.hound.yml +36 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +36 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.rdoc +113 -0
  10. data/Rakefile +12 -0
  11. data/lib/macmillan/utils.rb +16 -0
  12. data/lib/macmillan/utils/bundler/gem_helper.rb +9 -0
  13. data/lib/macmillan/utils/cucumber/cucumber_defaults.rb +12 -0
  14. data/lib/macmillan/utils/cucumber/webmock_helper.rb +9 -0
  15. data/lib/macmillan/utils/logger.rb +11 -0
  16. data/lib/macmillan/utils/logger/factory.rb +77 -0
  17. data/lib/macmillan/utils/logger/formatter.rb +48 -0
  18. data/lib/macmillan/utils/middleware.rb +7 -0
  19. data/lib/macmillan/utils/middleware/weak_etags.rb +33 -0
  20. data/lib/macmillan/utils/rails/statsd_instrumentation.rb +64 -0
  21. data/lib/macmillan/utils/rspec/rack_test_helper.rb +12 -0
  22. data/lib/macmillan/utils/rspec/rspec_defaults.rb +44 -0
  23. data/lib/macmillan/utils/rspec/webmock_helper.rb +13 -0
  24. data/lib/macmillan/utils/settings.rb +33 -0
  25. data/lib/macmillan/utils/settings/app_yaml_backend.rb +44 -0
  26. data/lib/macmillan/utils/settings/env_vars_backend.rb +13 -0
  27. data/lib/macmillan/utils/settings/key_not_found.rb +13 -0
  28. data/lib/macmillan/utils/settings/lookup.rb +29 -0
  29. data/lib/macmillan/utils/settings/value.rb +16 -0
  30. data/lib/macmillan/utils/statsd_controller_helper.rb +81 -0
  31. data/lib/macmillan/utils/statsd_decorator.rb +98 -0
  32. data/lib/macmillan/utils/statsd_middleware.rb +87 -0
  33. data/lib/macmillan/utils/statsd_stub.rb +43 -0
  34. data/lib/macmillan/utils/test_helpers/codeclimate_helper.rb +4 -0
  35. data/lib/macmillan/utils/test_helpers/fixture_loading_helper.rb +24 -0
  36. data/lib/macmillan/utils/test_helpers/simplecov_helper.rb +27 -0
  37. data/macmillan-utils.gemspec +33 -0
  38. data/spec/fixtures/config/application.yml +1 -0
  39. data/spec/lib/macmillan/utils/logger/factory_spec.rb +53 -0
  40. data/spec/lib/macmillan/utils/logger/formatter_spec.rb +57 -0
  41. data/spec/lib/macmillan/utils/middleware/weak_etags_spec.rb +30 -0
  42. data/spec/lib/macmillan/utils/settings_spec.rb +44 -0
  43. data/spec/lib/macmillan/utils/statsd_controller_helper_spec.rb +43 -0
  44. data/spec/lib/macmillan/utils/statsd_decorator_spec.rb +93 -0
  45. data/spec/lib/macmillan/utils/statsd_middleware_spec.rb +51 -0
  46. data/spec/spec_helper.rb +13 -0
  47. metadata +296 -0
@@ -0,0 +1,7 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Middleware
4
+ autoload :WeakEtags, 'macmillan/utils/middleware/weak_etags'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,33 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Middleware
4
+ ##
5
+ # Rack Middleware for use when proxying a rack app with Nginx using gzip compression.
6
+ #
7
+ # Nginx will convert ETags generated by our rack apps from STRONG etags, to WEAK etags,
8
+ # this middleware converts the ETags back to STRONG for use within the consuming app.
9
+ #
10
+ # @ref http://akshaykarle.com/blog/2014/09/17/rails-caching-with-nginx/
11
+ class WeakEtags
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ dup.process(env)
18
+ end
19
+
20
+ def process(env)
21
+ etag = env['HTTP_IF_NONE_MATCH']
22
+
23
+ if etag && etag.match(/^W\//)
24
+ env['HTTP_IF_NONE_MATCH'] = etag.gsub(/^W\//, '')
25
+ end
26
+
27
+ status, headers, body = @app.call(env)
28
+ [status, headers, body]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ require 'socket'
2
+
3
+ ##
4
+ # Default $statsD instrumentation for Rails applications
5
+ #
6
+ # ASSUMPTION: Your StatsD client is stored in a global variable $statsd
7
+ #
8
+ # Usage (in `config/initializers/statsd.rb`):
9
+ #
10
+ # require 'statsd-ruby'
11
+ # require 'macmillan/utils/statsd_decorator'
12
+ #
13
+ # $statsd = Statsd.new('http://statsd.example.com', 8080)
14
+ # $statsd = Macmillan::Utils::StatsdDecorator.new($statsd, Rails.env, Rails.logger)
15
+ #
16
+ # require 'macmillan/utils/rails/statsd_instrumentation'
17
+ #
18
+ # Options (Set as environment variables):
19
+ #
20
+ # STRIP_DOMAIN_FROM_HOST = domain name (string) to strip from the servers' host name
21
+ #
22
+ # Credit:
23
+ #
24
+ # The code below is knocked togther from ideas in...
25
+ # * http://railstips.org/blog/archives/2011/03/21/hi-my-name-is-john/
26
+ # * http://www.mikeperham.com/2012/08/25/using-statsd-with-rails/
27
+ # * http://37signals.com/svn/posts/3091-pssst-your-rails-application-has-a-secret-to-tell-you
28
+ #
29
+ # Notes:
30
+ #
31
+ # Uses ActiveSupport::Notifications to hook in some instrumentation...
32
+ # @see http://guides.rubyonrails.org/active_support_instrumentation.html
33
+ #
34
+ ActiveSupport::Notifications.subscribe /process_action.action_controller/ do |*args|
35
+ event = ActiveSupport::Notifications::Event.new(*args)
36
+ controller = event.payload[:controller].gsub('Controller', '_controller').gsub('::', '.')
37
+ action = event.payload[:action]
38
+ format = event.payload[:format]
39
+ format = 'other' unless %i(html json xml ris csv).include?(format)
40
+ status = event.payload[:status].to_i
41
+ status = '5xx' if status >= 500 && status <= 599
42
+ hostname = Socket.gethostname.downcase
43
+ hostname = hostname.gsub(ENV['STRIP_DOMAIN_FROM_HOST'], '') if ENV['STRIP_DOMAIN_FROM_HOST']
44
+ key = "controllers.#{controller}.#{action}.#{format}".downcase
45
+
46
+ # count reponses
47
+ $statsd.increment('http_status.overall')
48
+ $statsd.increment("http_status.#{status}")
49
+ $statsd.increment("#{key}.http_status.#{status}")
50
+ $statsd.increment("#{key}.#{hostname}.http_status.#{status}")
51
+
52
+ # only record timings on success
53
+ if status == 200
54
+ $statsd.timing('response_time', event.duration)
55
+
56
+ $statsd.timing("#{key}.response_time", event.duration)
57
+ $statsd.timing("#{key}.db_time", event.payload[:db_runtime])
58
+ $statsd.timing("#{key}.view_time", event.payload[:view_runtime])
59
+
60
+ $statsd.timing("#{key}.#{hostname}.response_time", event.duration)
61
+ $statsd.timing("#{key}.#{hostname}.db_time", event.payload[:db_runtime])
62
+ $statsd.timing("#{key}.#{hostname}.view_time", event.payload[:view_runtime])
63
+ end
64
+ end
@@ -0,0 +1,12 @@
1
+ require 'rack/test'
2
+
3
+ module RackTestHelper
4
+ def req_for(url, opts = {})
5
+ Rack::Request.new(env_for(url, opts))
6
+ end
7
+
8
+ def env_for(url, opts = {})
9
+ Rack::MockRequest.env_for(url, opts)
10
+ end
11
+ end
12
+ include RackTestHelper
@@ -0,0 +1,44 @@
1
+ def check_rubocop_and_hound
2
+ # ASSUMPTION: We are running the RSpec suite from the root of a project tree
3
+ update_rubocop = true
4
+ rubocop_file = '.rubocop.yml'
5
+ local_rubocop_file = File.join(Dir.getwd, rubocop_file)
6
+ local_hound_file = File.join(Dir.getwd, '.hound.yml')
7
+
8
+ if File.exist?(local_rubocop_file)
9
+ latest_rubocop_conf = File.read(File.expand_path("../../../../../#{rubocop_file}", __FILE__))
10
+ current_rubocop_conf = File.read(local_rubocop_file)
11
+ update_rubocop = false if current_rubocop_conf == latest_rubocop_conf
12
+ end
13
+
14
+ if !File.exist?(local_hound_file) || !File.symlink?(local_hound_file)
15
+ system "rm -f #{local_hound_file}"
16
+ system "ln -s #{rubocop_file} #{local_hound_file}"
17
+ end
18
+
19
+ if update_rubocop
20
+ puts 'WARNING: You do not have the latest set of rubocop style preferences.'
21
+ puts ' These have now been updated for you. :)'
22
+ puts ''
23
+ puts ' You can run RSpec again now.'
24
+ puts ''
25
+ puts " Don't forget to commit the '.rubocop.yml' and '.hound.yml' files to git!"
26
+
27
+ File.open(local_rubocop_file, 'w') do |file|
28
+ file.print latest_rubocop_conf
29
+ end
30
+
31
+ fail RuntimeError, '...'
32
+ end
33
+ end
34
+
35
+ RSpec.configure do |config|
36
+ config.order = 'random'
37
+
38
+ # Exit the suite on the first failure
39
+ config.fail_fast = true if ENV['FAIL_FAST']
40
+
41
+ config.before(:suite) do
42
+ check_rubocop_and_hound
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ require 'webmock/rspec'
2
+
3
+ WebMock.disable_net_connect!(allow_localhost: true, allow: [/codeclimate/])
4
+
5
+ RSpec.configure do |config|
6
+ config.after(:each) do
7
+ WebMock.reset!
8
+ end
9
+
10
+ config.after(:suite) do
11
+ WebMock.disable!
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Settings
4
+ autoload :AppYamlBackend, 'macmillan/utils/settings/app_yaml_backend'
5
+ autoload :EnvVarsBackend, 'macmillan/utils/settings/env_vars_backend'
6
+ autoload :Lookup, 'macmillan/utils/settings/lookup'
7
+ autoload :Value, 'macmillan/utils/settings/value'
8
+ autoload :KeyNotFound, 'macmillan/utils/settings/key_not_found'
9
+
10
+ class KeyNotFoundError < StandardError; end
11
+
12
+ class << self
13
+ # Get an instance of the settings looker-upper
14
+ def instance
15
+ @instance ||= begin
16
+ backend_instances = backends.map(&:new)
17
+ Lookup.new(backend_instances)
18
+ end
19
+ end
20
+
21
+ # Backends must respond to the following interface:
22
+ # # `.new` :: Return an instance of the backend
23
+ # # `#get key`:: Return a Value for the key.
24
+ # :: If there's no setting, return
25
+ # :: a KeyNotFound
26
+ #
27
+ attr_accessor :backends
28
+ end
29
+
30
+ self.backends = [EnvVarsBackend, AppYamlBackend]
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ require 'yaml'
2
+
3
+ module Macmillan
4
+ module Utils
5
+ module Settings
6
+ class AppYamlBackend
7
+ def get(key)
8
+ return build_value(key) if yaml.key?(key)
9
+ KeyNotFound.new(key, self, key)
10
+ end
11
+
12
+ private
13
+
14
+ def build_value(key)
15
+ Value.new(key, yaml[key], self, key)
16
+ end
17
+
18
+ def yaml
19
+ @yaml ||= begin
20
+ YAML.load(File.open(application_yml_path))
21
+ end
22
+ end
23
+
24
+ def application_yml_path
25
+ search_pattern = File.join('config', 'application.yml')
26
+ here = File.expand_path(Dir.pwd)
27
+ path_components = here.split(/\//)
28
+ found_path = nil
29
+
30
+ path_components.size.downto(1) do |path_size|
31
+ break if found_path
32
+ search_path = path_components[0, path_size]
33
+ search_file = File.join(search_path, search_pattern)
34
+ found_path = search_file if File.exist?(search_file)
35
+ end
36
+
37
+ fail 'cannot find application.yml' if found_path.nil?
38
+
39
+ found_path
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,13 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Settings
4
+ class EnvVarsBackend
5
+ def get(key)
6
+ backend_key = key.to_s.upcase
7
+ return KeyNotFound.new(key, self, backend_key) unless ENV.key?(backend_key)
8
+ Value.new(key, ENV[backend_key].dup, self, backend_key)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Settings
4
+ class KeyNotFound
5
+ def initialize(lookup_key, backend, backend_key)
6
+ @lookup_key = lookup_key
7
+ @backend = backend
8
+ @backend_key = backend_key
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Settings
4
+ class Lookup
5
+ def initialize(backends)
6
+ @backends = backends
7
+ end
8
+
9
+ def lookup(key)
10
+ value = nil
11
+
12
+ @backends.each do |backend|
13
+ break if value
14
+ result = backend.get(key)
15
+ value = result.value unless result.is_a?(KeyNotFound)
16
+ end
17
+
18
+ fail KeyNotFoundError.new("Cannot find a settings value for #{key}") unless value
19
+
20
+ value
21
+ end
22
+
23
+ # Backwards compatibility: in the past this has been used like a Hash
24
+ alias_method :[], :lookup
25
+ alias_method :fetch, :lookup
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ module Macmillan
2
+ module Utils
3
+ module Settings
4
+ class Value
5
+ attr_reader :value
6
+
7
+ def initialize(lookup_key, value, backend, backend_key)
8
+ @lookup_key = lookup_key
9
+ @value = value
10
+ @backend = backend
11
+ @backend_key = backend_key
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,81 @@
1
+ require_relative 'statsd_middleware'
2
+
3
+ module Macmillan
4
+ module Utils
5
+ ##
6
+ # Helper functions for working with {Macmillan::Utils::StatsdMiddleware}
7
+ # in Rack based web applications.
8
+ #
9
+ # This code is heavily inspired by {https://github.com/mleinart/sinatra-statsd-helper sinatra-statsd-helper}
10
+ #
11
+ # == Usage:
12
+ #
13
+ # Add 'macmillan-utils' to your Gemfile:
14
+ #
15
+ # gem 'macmillan-utils', require: false
16
+ #
17
+ # First, setup the {Macmillan::Utils::StatsdMiddleware} as described in its
18
+ # documentation. Then simply include this module in your controller classes.
19
+ #
20
+ # i.e. in Sinatra
21
+ #
22
+ # require 'macmillan/utils/statsd_controller_helper'
23
+ #
24
+ # class Server < Sinatra::Base
25
+ # include Macmillan::Utils::StatsdControllerHelper
26
+ #
27
+ # get '/' do
28
+ # add_statsd_timer('get.homepage') # sends a timer to the stat 'get.homepage' with the timing of the request
29
+ # end
30
+ #
31
+ # get '/inc' do
32
+ # add_statsd_increment('get.inc') # sends an increment to the stat 'get.inc'
33
+ # end
34
+ #
35
+ # get '/both' do
36
+ # add_statsd_timer_and_increment('get.both') # sends both an timer and increment stat to 'get.both'
37
+ # end
38
+ # end
39
+ #
40
+ # Rails works identically:
41
+ #
42
+ # require 'macmillan/utils/statsd_controller_helper'
43
+ #
44
+ # class SiteController < ApplicationController
45
+ # include Macmillan::Utils::StatsdControllerHelper
46
+ #
47
+ # def index
48
+ # add_statsd_timer_and_increment('get.site_controller.index')
49
+ # end
50
+ # end
51
+ #
52
+ module StatsdControllerHelper
53
+ module_function
54
+
55
+ ##
56
+ # Send a timer stat to statsd (with the timing of the whole rack request)
57
+ #
58
+ # @param key [String] the statsd/graphite statistic name/key
59
+ def add_statsd_timer(key)
60
+ request.env[::Macmillan::Utils::StatsdMiddleware::TIMERS] << key
61
+ end
62
+
63
+ ##
64
+ # Send an increment stat to statsd
65
+ #
66
+ # @param key [String] the statsd/graphite statistic name/key
67
+ def add_statsd_increment(key)
68
+ request.env[::Macmillan::Utils::StatsdMiddleware::INCREMENTS] << key
69
+ end
70
+
71
+ ##
72
+ # Send both a timer and an increment stat to statsd
73
+ #
74
+ # @param key [String] the statsd/graphite statistic name/key
75
+ def add_statsd_timer_and_increment(key)
76
+ add_statsd_timer(key)
77
+ add_statsd_increment(key)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,98 @@
1
+ module Macmillan
2
+ module Utils
3
+ ##
4
+ # Utility class to wrap the Statsd class from {http://rubygems.org/gems/statsd-ruby statsd-ruby}.
5
+ #
6
+ # This will allow you to log Statsd messages to your logs, but only
7
+ # really send messages to the StatsD server when running in a
8
+ # 'production' environment.
9
+ #
10
+ # === Usage:
11
+ #
12
+ # Add 'statsd-ruby' and 'macmillan-utils' to your Gemfile:
13
+ #
14
+ # gem 'statsd-ruby'
15
+ # gem 'macmillan-utils', require: false
16
+ #
17
+ # Then in your code:
18
+ #
19
+ # require 'statsd'
20
+ # require 'macmillan/utils/statsd_decorator'
21
+ #
22
+ # statsd = Statsd.new('http://statsd.example.com', 8080)
23
+ # statsd = Macmillan::Utils::StatsdDecorator.new(statsd, ENV['RACK_ENV'])
24
+ #
25
+ # i.e. when using rails, use the rails env and logger:
26
+ #
27
+ # statsd = Statsd.new('http://statsd.example.com', 8080)
28
+ # statsd = Macmillan::Utils::StatsdDecorator.new(statsd, Rails.env, Rails.logger)
29
+ #
30
+ # @see http://rubygems.org/gems/statsd-ruby
31
+ #
32
+ class StatsdDecorator < SimpleDelegator
33
+ attr_accessor :env, :logger
34
+
35
+ ##
36
+ # Builds a new instance of StatsdDecorator
37
+ #
38
+ # @param delegatee [Statsd] a Statsd object
39
+ # @param env [String] the current application environment - i.e. 'development' or 'production'
40
+ # @param logger [Logger] a Logger object
41
+ # @return [Statsd] the decorated Statsd class
42
+ #
43
+ def initialize(delegatee, env = 'development', logger = Macmillan::Utils::Logger::Factory.build_logger)
44
+ @env = env
45
+ @logger = logger
46
+ super(delegatee)
47
+ end
48
+
49
+ def increment(stat, sample_rate = 1)
50
+ log_stat %{increment - "#{stat}" (sample_rate: #{sample_rate})}
51
+ super if send_to_delegatee?
52
+ end
53
+
54
+ def decrement(stat, sample_rate = 1)
55
+ log_stat %{decrement - "#{stat}" (sample_rate: #{sample_rate})}
56
+ super if send_to_delegatee?
57
+ end
58
+
59
+ def count(stat, count, sample_rate = 1)
60
+ log_stat %{count - "#{stat}" #{count} (sample_rate: #{sample_rate})}
61
+ super if send_to_delegatee?
62
+ end
63
+
64
+ def guage(stat, value, sample_rate = 1)
65
+ log_stat %{gauge - "#{stat}" #{value} (sample_rate: #{sample_rate})}
66
+ super if send_to_delegatee?
67
+ end
68
+
69
+ def set(stat, value, sample_rate = 1)
70
+ log_stat %{set - "#{stat}" #{value} (sample_rate: #{sample_rate})}
71
+ super if send_to_delegatee?
72
+ end
73
+
74
+ def timing(stat, ms, sample_rate = 1)
75
+ log_stat %{timing - "#{stat}" #{ms}ms (sample_rate: #{sample_rate})}
76
+ super if send_to_delegatee?
77
+ end
78
+
79
+ def time(stat, sample_rate = 1, &block)
80
+ start = Time.now
81
+ result = block.call
82
+ duration = ((Time.now - start) * 1000).round
83
+ timing(stat, duration, sample_rate)
84
+ result
85
+ end
86
+
87
+ private
88
+
89
+ def log_stat(msg)
90
+ logger.debug "[StatsD] #{msg}"
91
+ end
92
+
93
+ def send_to_delegatee?
94
+ env == 'production'
95
+ end
96
+ end
97
+ end
98
+ end