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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.hound.yml +36 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.rdoc +113 -0
- data/Rakefile +12 -0
- data/lib/macmillan/utils.rb +16 -0
- data/lib/macmillan/utils/bundler/gem_helper.rb +9 -0
- data/lib/macmillan/utils/cucumber/cucumber_defaults.rb +12 -0
- data/lib/macmillan/utils/cucumber/webmock_helper.rb +9 -0
- data/lib/macmillan/utils/logger.rb +11 -0
- data/lib/macmillan/utils/logger/factory.rb +77 -0
- data/lib/macmillan/utils/logger/formatter.rb +48 -0
- data/lib/macmillan/utils/middleware.rb +7 -0
- data/lib/macmillan/utils/middleware/weak_etags.rb +33 -0
- data/lib/macmillan/utils/rails/statsd_instrumentation.rb +64 -0
- data/lib/macmillan/utils/rspec/rack_test_helper.rb +12 -0
- data/lib/macmillan/utils/rspec/rspec_defaults.rb +44 -0
- data/lib/macmillan/utils/rspec/webmock_helper.rb +13 -0
- data/lib/macmillan/utils/settings.rb +33 -0
- data/lib/macmillan/utils/settings/app_yaml_backend.rb +44 -0
- data/lib/macmillan/utils/settings/env_vars_backend.rb +13 -0
- data/lib/macmillan/utils/settings/key_not_found.rb +13 -0
- data/lib/macmillan/utils/settings/lookup.rb +29 -0
- data/lib/macmillan/utils/settings/value.rb +16 -0
- data/lib/macmillan/utils/statsd_controller_helper.rb +81 -0
- data/lib/macmillan/utils/statsd_decorator.rb +98 -0
- data/lib/macmillan/utils/statsd_middleware.rb +87 -0
- data/lib/macmillan/utils/statsd_stub.rb +43 -0
- data/lib/macmillan/utils/test_helpers/codeclimate_helper.rb +4 -0
- data/lib/macmillan/utils/test_helpers/fixture_loading_helper.rb +24 -0
- data/lib/macmillan/utils/test_helpers/simplecov_helper.rb +27 -0
- data/macmillan-utils.gemspec +33 -0
- data/spec/fixtures/config/application.yml +1 -0
- data/spec/lib/macmillan/utils/logger/factory_spec.rb +53 -0
- data/spec/lib/macmillan/utils/logger/formatter_spec.rb +57 -0
- data/spec/lib/macmillan/utils/middleware/weak_etags_spec.rb +30 -0
- data/spec/lib/macmillan/utils/settings_spec.rb +44 -0
- data/spec/lib/macmillan/utils/statsd_controller_helper_spec.rb +43 -0
- data/spec/lib/macmillan/utils/statsd_decorator_spec.rb +93 -0
- data/spec/lib/macmillan/utils/statsd_middleware_spec.rb +51 -0
- data/spec/spec_helper.rb +13 -0
- 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,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
|