macmillan-utils 1.0.11

Sign up to get free protection for your applications and to get access to all the features.
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,87 @@
1
+ require 'set'
2
+
3
+ module Macmillan
4
+ module Utils
5
+ ##
6
+ # Rack Middleware for sending request timings and other statistics to StatsD.
7
+ #
8
+ # This code is heavily inspired by https://github.com/manderson26/statsd/blob/master/lib/statsd/middleware.rb
9
+ #
10
+ # == Usage:
11
+ #
12
+ # In config.ru:
13
+ #
14
+ # require 'statsd'
15
+ # require 'macmillan/utils/statsd_decorator'
16
+ # require 'macmillan/utils/statsd_middleware'
17
+ #
18
+ # statsd = Statsd.new('http://statsd.example.com', 8080)
19
+ # statsd = Macmillan::Utils::StatsdDecorator.new(statsd, ENV['RACK_ENV'])
20
+ #
21
+ # use Macmillan::Utils::StatsdMiddleware, client: statsd
22
+ #
23
+ # By default this middleware will record timer and increment stats for all requests under
24
+ # the statsd/graphite namespace 'rack.' - i.e.
25
+ #
26
+ # * rack.request - timers and increment per request
27
+ # * rack.status_code.<status code> - increment per request
28
+ # * rack.exception - increment upon error
29
+ #
30
+ # Facilities are provided via {Macmillan::Utils::StatsdControllerHelper} to also log
31
+ # per-route metrics via this middleware.
32
+ #
33
+ class StatsdMiddleware
34
+ NAMESPACE = 'rack'.freeze
35
+ TIMERS = 'statsd.timers'.freeze
36
+ INCREMENTS = 'statsd.increments'.freeze
37
+
38
+ def initialize(app, opts = {})
39
+ fail ArgumentError, 'You must supply a StatsD client' unless opts[:client]
40
+
41
+ @app = app
42
+ @client = opts[:client]
43
+ end
44
+
45
+ def call(env)
46
+ dup.process(env)
47
+ end
48
+
49
+ def process(env)
50
+ setup(env)
51
+
52
+ (status, headers, body), response_time = call_with_timing(env)
53
+
54
+ record_metrics(env, status, response_time)
55
+
56
+ [status, headers, body]
57
+ rescue => error
58
+ @client.increment("#{NAMESPACE}.exception")
59
+ raise error
60
+ end
61
+
62
+ private
63
+
64
+ def setup(env)
65
+ env[TIMERS] = Set.new(['request'])
66
+ env[INCREMENTS] = Set.new(['request'])
67
+ end
68
+
69
+ def record_metrics(env, status, response_time)
70
+ env[TIMERS].each do |key|
71
+ @client.timing("#{NAMESPACE}.timers.#{key}", response_time)
72
+ end
73
+
74
+ env[INCREMENTS].each do |key|
75
+ @client.increment("#{NAMESPACE}.increments.#{key}")
76
+ @client.increment("#{NAMESPACE}.http_status.#{key}.#{status}")
77
+ end
78
+ end
79
+
80
+ def call_with_timing(env)
81
+ start = Time.now
82
+ result = @app.call(env)
83
+ [result, ((Time.now - start) * 1000).round]
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,43 @@
1
+ module Macmillan
2
+ module Utils
3
+ ##
4
+ # A test helper class for stubbing out interaction with StatsD.
5
+ #
6
+ # === Usage (in spec/spec_helper.rb):
7
+ #
8
+ # require 'macmillan/utils/statsd_stub'
9
+ #
10
+ # RSpec.configure do |config|
11
+ # config.before(:suite) do
12
+ # $statsd = Macmillan::Utils::StatsdStub.new
13
+ # end
14
+ # end
15
+ #
16
+ # @see Macmillan::Utils::StatsdDecorator
17
+ # @see http://rubygems.org/gems/statsd-ruby
18
+ #
19
+ class StatsdStub
20
+ def increment(_stat, _sample_rate = 1)
21
+ end
22
+
23
+ def decrement(_stat, _sample_rate = 1)
24
+ end
25
+
26
+ def count(_stat, _count, _sample_rate = 1)
27
+ end
28
+
29
+ def guage(_stat, _value, _sample_rate = 1)
30
+ end
31
+
32
+ def set(_stat, _value, _sample_rate = 1)
33
+ end
34
+
35
+ def timing(_stat, _ms, _sample_rate = 1)
36
+ end
37
+
38
+ def time(_stat, _sample_rate = 1)
39
+ yield
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ if ENV['CODECLIMATE_REPO_TOKEN']
2
+ require 'codeclimate-test-reporter'
3
+ CodeClimate::TestReporter.start
4
+ end
@@ -0,0 +1,24 @@
1
+ module FixtureLoading
2
+ # ASSUMPTION: We are running the test suite from the root of a project tree
3
+
4
+ def load_yaml_data(filename)
5
+ filename << '.yml' unless filename.end_with?('.yml')
6
+ YAML.load(load_text_data(filename))
7
+ end
8
+
9
+ def load_json_data(filename)
10
+ filename << '.json' unless filename.end_with?('.json')
11
+ JSON.parse(load_text_data(filename))
12
+ end
13
+
14
+ def load_text_data(filename)
15
+ File.open(get_file_name(filename), 'rb').read
16
+ end
17
+
18
+ private
19
+
20
+ def get_file_name(filename)
21
+ File.join(Dir.getwd, 'spec/support/fixtures', filename)
22
+ end
23
+ end
24
+ include FixtureLoading
@@ -0,0 +1,27 @@
1
+ if ENV['USE_SIMPLECOV']
2
+ require 'simplecov'
3
+ require 'simplecov-rcov'
4
+
5
+ formatters = [
6
+ SimpleCov::Formatter::HTMLFormatter,
7
+ SimpleCov::Formatter::RcovFormatter
8
+ ]
9
+
10
+ if ENV['CODECLIMATE_REPO_TOKEN']
11
+ require 'codeclimate-test-reporter'
12
+ formatters << CodeClimate::TestReporter::Formatter
13
+ end
14
+
15
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[*formatters]
16
+
17
+ unless ENV['DO_NOT_START_SIMPLECOV']
18
+ mode = nil
19
+ mode = 'rails' if ENV['RAILS_ENV']
20
+
21
+ SimpleCov.start mode do
22
+ load_profile 'test_frameworks'
23
+ merge_timeout 3600
24
+ coverage_dir 'artifacts/coverage'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+
4
+ Gem::Specification.new do |spec|
5
+ spec.name = 'macmillan-utils'
6
+ spec.version = "1.0.#{ENV['BUILD_NUMBER'] || 'dev'}"
7
+ spec.authors = ['Macmillan Science and Education (New Publsihing Platforms)']
8
+ spec.email = ['npp-developers@macmillan.com']
9
+ spec.summary = 'A collection of useful patterns we (Macmillan Science and Education) use in our Ruby applications.'
10
+ spec.homepage = 'https://github.com/nature/macmillan-utils'
11
+ spec.license = 'MIT'
12
+
13
+ spec.files = `git ls-files -z`.split("\x0")
14
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
15
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.require_paths = ['lib']
17
+
18
+ spec.add_development_dependency 'bundler', '~> 1.6'
19
+ spec.add_development_dependency 'rake'
20
+ spec.add_development_dependency 'yard'
21
+ spec.add_development_dependency 'pry'
22
+ spec.add_development_dependency 'rack-test'
23
+
24
+ spec.add_dependency 'rspec'
25
+ spec.add_dependency 'simplecov'
26
+ spec.add_dependency 'simplecov-rcov'
27
+ spec.add_dependency 'codeclimate-test-reporter'
28
+ spec.add_dependency 'webmock'
29
+ spec.add_dependency 'multi_test'
30
+ spec.add_dependency 'syslog-logger'
31
+ spec.add_dependency 'rubocop'
32
+ spec.add_dependency 'geminabox'
33
+ end
@@ -0,0 +1 @@
1
+ process_pid: 1234
@@ -0,0 +1,53 @@
1
+ require 'spec_helper'
2
+
3
+ describe Macmillan::Utils::Logger::Factory do
4
+ describe '#build_logger' do
5
+ context 'for a syslog logger' do
6
+ subject { Macmillan::Utils::Logger::Factory.build_logger(:syslog, tag: 'myapp', facility: 2) }
7
+
8
+ it 'returns a Logger::Syslog object' do
9
+ expect(subject).to be_an_instance_of(Logger::Syslog)
10
+ end
11
+
12
+ it 'allows you to configure the syslog tag and facility' do
13
+ expect(Logger::Syslog).to receive(:new).with('myapp', Syslog::LOG_LOCAL2).and_call_original
14
+ subject
15
+ end
16
+
17
+ it 'aliases logger#write to logger#info' do
18
+ expect(subject).to respond_to(:write)
19
+ end
20
+ end
21
+
22
+ context 'for a standard Logger' do
23
+ subject { Macmillan::Utils::Logger::Factory.build_logger(:logger) }
24
+
25
+ it 'returns a Logger object' do
26
+ expect(subject).to be_an_instance_of(Logger)
27
+ end
28
+
29
+ it 'logs to STDOUT by default' do
30
+ expect(Logger).to receive(:new).with($stdout).and_call_original
31
+ subject
32
+ end
33
+
34
+ it 'allows you to configure the log target' do
35
+ expect(Logger).to receive(:new).with('foo.log').and_call_original
36
+ Macmillan::Utils::Logger::Factory.build_logger(:logger, target: 'foo.log')
37
+ end
38
+ end
39
+
40
+ context 'for a null logger' do
41
+ subject { Macmillan::Utils::Logger::Factory.build_logger(:null) }
42
+
43
+ it 'returns a Logger object' do
44
+ expect(subject).to be_an_instance_of(Logger)
45
+ end
46
+
47
+ it 'builds a logger object that points to /dev/null' do
48
+ expect(Logger).to receive(:new).with('/dev/null').and_call_original
49
+ subject
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Macmillan::Utils::Logger::Formatter do
4
+ let(:msg) { 'testing' }
5
+ let(:prefix) { nil }
6
+ let(:target) { File.open('/dev/null', 'w+') }
7
+
8
+ subject { Macmillan::Utils::Logger::Formatter.new(prefix) }
9
+
10
+ let(:logger) do
11
+ log = Macmillan::Utils::Logger::Factory.build_logger(:logger, target: target)
12
+ log.formatter = subject
13
+ log
14
+ end
15
+
16
+ describe '#call' do
17
+ it 'is called by the logger object' do
18
+ expect(target).to receive(:write).once
19
+ expect(subject).to receive(:call).once
20
+ logger.info 'this is a test'
21
+ end
22
+
23
+ context 'when a prefix is set' do
24
+ let(:prefix) { 'WEEEE' }
25
+
26
+ it 'is put in front of the log message' do
27
+ expect(target).to receive(:write).with("#{prefix} [ INFO]: #{msg}\n").once
28
+ logger.info msg
29
+ end
30
+ end
31
+
32
+ context 'when the log message is a string' do
33
+ it 'returns the string' do
34
+ expect(target).to receive(:write).with("[ INFO]: #{msg}\n").once
35
+ logger.info msg
36
+ end
37
+ end
38
+
39
+ context 'when the log message is an exception' do
40
+ it 'returns full details of the exception' do
41
+ ex = StandardError.new('qwerty')
42
+ allow(ex).to receive(:backtrace).and_return(%w(foo bar baz quux))
43
+ expect(target).to receive(:write).with("[ INFO]: qwerty (StandardError)\nfoo\nbar\nbaz\nquux\n").once
44
+ logger.info ex
45
+ end
46
+ end
47
+
48
+ context 'when the log message is NOT a string or exception' do
49
+ it 'retuns object.inspect' do
50
+ ex = Array.new
51
+ expect(ex).to receive(:inspect).once
52
+
53
+ logger.info ex
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,30 @@
1
+ require 'spec_helper'
2
+
3
+ describe Macmillan::Utils::Middleware::WeakEtags do
4
+ let(:etag) { 'W/"qwerty"' }
5
+ let(:app) { ->(env) { [200, env, 'app'] } }
6
+
7
+ let(:request) do
8
+ req = req_for('http://example.com')
9
+ req.env['HTTP_IF_NONE_MATCH'] = etag
10
+ req
11
+ end
12
+
13
+ subject { Macmillan::Utils::Middleware::WeakEtags.new(app) }
14
+
15
+ context 'when using Weak ETags' do
16
+ it 'removes the "W/" from the header' do
17
+ _status, headers, _body = subject.call(request.env)
18
+ expect(headers['HTTP_IF_NONE_MATCH']).to eq('"qwerty"')
19
+ end
20
+ end
21
+
22
+ context 'when using Strong ETags' do
23
+ let(:etag) { '"qwerty"' }
24
+
25
+ it 'does not modify the header' do
26
+ _status, headers, _body = subject.call(request.env)
27
+ expect(headers['HTTP_IF_NONE_MATCH']).to eq('"qwerty"')
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe Macmillan::Utils::Settings do
4
+ let(:env_vars_backend) { Macmillan::Utils::Settings::EnvVarsBackend.new }
5
+ let(:app_yaml_backend) { Macmillan::Utils::Settings::AppYamlBackend.new }
6
+ let(:backends) { [env_vars_backend, app_yaml_backend] }
7
+
8
+ subject { Macmillan::Utils::Settings::Lookup.new(backends) }
9
+
10
+ it 'lookups variables from the local environment' do
11
+ ENV['FOO'] = 'bar'
12
+ var = subject.lookup 'foo'
13
+ expect(var).to eql 'bar'
14
+ end
15
+
16
+ it 'raises an error if the lookup fails' do
17
+ looker_upper = Macmillan::Utils::Settings::Lookup.new([env_vars_backend])
18
+
19
+ expect do
20
+ looker_upper.lookup 'baz'
21
+ end.to raise_error(Macmillan::Utils::Settings::KeyNotFoundError)
22
+ end
23
+
24
+ context 'when using an application.yml file' do
25
+ context 'and the file exists' do
26
+ it 'lookups variables from the local application yml' do
27
+ fixtures_dir = File.expand_path('../../../../fixtures', __FILE__)
28
+
29
+ Dir.chdir(fixtures_dir) do
30
+ var = subject.lookup 'process_pid'
31
+ expect(var).to eql(1234)
32
+ end
33
+ end
34
+ end
35
+
36
+ context 'but the file does not exist' do
37
+ it 'raises an appropriate error' do
38
+ expect do
39
+ subject.lookup 'wibble'
40
+ end.to raise_error('cannot find application.yml')
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ describe Macmillan::Utils::StatsdControllerHelper do
4
+ let(:request) { req_for('http://example.com') }
5
+ let(:timers) { [] }
6
+ let(:increments) { [] }
7
+
8
+ class TestSubject
9
+ attr_accessor :request
10
+
11
+ include Macmillan::Utils::StatsdControllerHelper
12
+ end
13
+
14
+ subject { TestSubject.new }
15
+
16
+ before do
17
+ subject.request = request
18
+ request.env[::Macmillan::Utils::StatsdMiddleware::TIMERS] = timers
19
+ request.env[::Macmillan::Utils::StatsdMiddleware::INCREMENTS] = increments
20
+ end
21
+
22
+ describe '#add_statsd_timer' do
23
+ it 'adds a key to the correct env variable' do
24
+ subject.send(:add_statsd_timer, 'woo')
25
+ expect(request.env[::Macmillan::Utils::StatsdMiddleware::TIMERS]).to eq(['woo'])
26
+ end
27
+ end
28
+
29
+ describe '#add_statsd_increment' do
30
+ it 'adds a key to the correct env variable' do
31
+ subject.send(:add_statsd_increment, 'waa')
32
+ expect(request.env[::Macmillan::Utils::StatsdMiddleware::INCREMENTS]).to eq(['waa'])
33
+ end
34
+ end
35
+
36
+ describe '#add_statsd_timer_and_increment' do
37
+ it 'adds a key to both env variables' do
38
+ subject.send(:add_statsd_timer_and_increment, 'wee')
39
+ expect(request.env[::Macmillan::Utils::StatsdMiddleware::TIMERS]).to eq(['wee'])
40
+ expect(request.env[::Macmillan::Utils::StatsdMiddleware::INCREMENTS]).to eq(['wee'])
41
+ end
42
+ end
43
+ end