macmillan-utils 1.0.11
Sign up to get free protection for your applications and to get access to all the features.
- 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
|