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,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