loga 2.5.1 → 2.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2edeb506c587fc55a4a3569aa4fa50af23c0080d0409da3de8aaf4f8f9df60f1
4
- data.tar.gz: 98ef790dd9c26f71d3a5f6c656d1a1663db56a90da28fa49f03da239bbc57d80
3
+ metadata.gz: 5366cef0d19c0384219e570b910635edeced8c13f7d27a7726f76397de78b492
4
+ data.tar.gz: 1015b91450e060ca3d705a93c50decf6111136a51d4819429f3be45f97687e53
5
5
  SHA512:
6
- metadata.gz: 62a82d5f31b3b4a1d29464d51e15ef1346ee9a7cc0fddc8047703fbdd0d041c089fc9423ed02fcdf81f0a5be6f329893d5081003ef347fbf5c8f032bd4f7dabe
7
- data.tar.gz: a70770e9de89a0357a25c1290bafff87b0422efc1b9dd7a6861004bacceb6acc9db67f10bd9c41e1bc5ff0178a63b1142ab5efe3cca4cb18745808deb550b1d4
6
+ metadata.gz: 151f8dddb93eeaf02ed792e45d51e7c694f349d6d397d18602ac6928953844f7047abaf7ae31b681cfa068f75842799d54eb90a948c75cead9e3b2edd1200175
7
+ data.tar.gz: 80a2b4f443aaea2403388de748fb181aeea34c81f6e693cc08b50b6b9893a48bcfed447c747d0f241823d99904b28772b64e7495870ac03426b71b0be6991f97
data/.circleci/config.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  basic_build: &basic_build
2
2
  working_directory: ~/loga
3
3
  docker:
4
- - image: circleci/ruby:2.5.1
4
+ - image: cimg/ruby:2.7.5
5
5
 
6
6
  test_build: &test_build
7
7
  working_directory: ~/loga
@@ -25,43 +25,11 @@ test_build: &test_build
25
25
  at: ./tmp
26
26
  - run: RACK_ENV=development bundle exec appraisal rspec
27
27
  - run: RACK_ENV=production bundle exec appraisal rspec
28
- - run: ./tmp/cc-test-reporter format-coverage -t simplecov -o $(ruby -e 'puts "tmp/codeclimate.#{RUBY_VERSION}.json"') coverage/.resultset.json
29
- - persist_to_workspace:
30
- root: tmp
31
- paths:
32
- - codeclimate.*.json
33
28
  - store_artifacts:
34
29
  path: coverage
35
30
 
36
31
  version: 2
37
32
  jobs:
38
- build:
39
- <<: *basic_build
40
- steps:
41
- - run:
42
- name: Download cc-test-reporter
43
- command: |
44
- mkdir -p tmp/
45
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./tmp/cc-test-reporter
46
- chmod +x ./tmp/cc-test-reporter
47
- - persist_to_workspace:
48
- root: ./tmp
49
- paths:
50
- - cc-test-reporter
51
- upload-coverage:
52
- <<: *basic_build
53
- steps:
54
- - attach_workspace:
55
- at: ./tmp
56
- - run:
57
- name: Upload coverage results to Code Climate
58
- command: |
59
- ./tmp/cc-test-reporter sum-coverage tmp/codeclimate.*.json -o tmp/codeclimate.total.json
60
- ./tmp/cc-test-reporter upload-coverage -i tmp/codeclimate.total.json -r $CODECLIMATE_REPO_TOKEN
61
- ruby-2.3:
62
- docker:
63
- - image: circleci/ruby:2.3
64
- <<: *test_build
65
33
  ruby-2.4:
66
34
  docker:
67
35
  - image: circleci/ruby:2.4
@@ -74,6 +42,14 @@ jobs:
74
42
  docker:
75
43
  - image: circleci/ruby:2.6
76
44
  <<: *test_build
45
+ ruby-2.7:
46
+ docker:
47
+ - image: cimg/ruby:2.7
48
+ <<: *test_build
49
+ ruby-3.0:
50
+ docker:
51
+ - image: cimg/ruby:3.0
52
+ <<: *test_build
77
53
  rubocop:
78
54
  <<: *basic_build
79
55
  steps:
@@ -103,48 +79,32 @@ workflows:
103
79
  version: 2
104
80
  test-then-push-gem:
105
81
  jobs:
106
- - build:
107
- filters:
108
- tags:
109
- only: /.*/
110
82
  - rubocop:
111
83
  filters:
112
84
  tags:
113
85
  only: /.*/
114
- - ruby-2.3:
115
- filters:
116
- tags:
117
- only: /.*/
118
- requires:
119
- - build
120
86
  - ruby-2.4:
121
87
  filters:
122
88
  tags:
123
89
  only: /.*/
124
- requires:
125
- - build
126
90
  - ruby-2.5:
127
91
  filters:
128
92
  tags:
129
93
  only: /.*/
130
- requires:
131
- - build
132
94
  - ruby-2.6:
133
95
  filters:
134
96
  tags:
135
97
  only: /.*/
136
- requires:
137
- - build
138
- - upload-coverage:
98
+ - ruby-2.7:
99
+ filters:
100
+ tags:
101
+ only: /.*/
102
+ - ruby-3.0:
139
103
  filters:
140
104
  tags:
141
105
  only: /.*/
142
- requires:
143
- - ruby-2.3
144
- - ruby-2.4
145
- - ruby-2.5
146
- - ruby-2.6
147
106
  - push-to-rubygems:
107
+ context: org-rubygems
148
108
  filters:
149
109
  tags:
150
110
  only: /^v\d.+/
@@ -152,7 +112,8 @@ workflows:
152
112
  ignore: /.*/
153
113
  requires:
154
114
  - rubocop
155
- - ruby-2.3
156
115
  - ruby-2.4
157
116
  - ruby-2.5
158
117
  - ruby-2.6
118
+ - ruby-2.7
119
+ - ruby-3.0
data/Appraisals CHANGED
@@ -8,26 +8,42 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.4.0')
8
8
  end
9
9
  end
10
10
 
11
- appraise 'rails42' do
12
- gem 'rails', '~> 4.2.0'
13
- end
11
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.7.0')
12
+ appraise 'rails42' do
13
+ gem 'rails', '~> 4.2.0'
14
+ end
14
15
 
15
- appraise 'sinatra14' do
16
- gem 'sinatra', '~> 1.4.0'
17
- end
16
+ appraise 'rails50' do
17
+ gem 'rails', '~> 5.0.0'
18
+ end
18
19
 
19
- appraise 'rails50' do
20
- gem 'rails', '~> 5.0.0'
20
+ appraise 'rails52' do
21
+ gem 'rails', '~> 5.2.0'
22
+ end
21
23
  end
22
24
 
23
- appraise 'rails52' do
24
- gem 'rails', '~> 5.2.0'
25
+ appraise 'sinatra14' do
26
+ gem 'sinatra', '~> 1.4.0'
25
27
  end
26
28
 
27
29
  if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.5.0')
28
30
  appraise 'rails60' do
29
31
  gem 'rails', '~> 6.0.0'
30
32
  end
33
+
34
+ appraise 'sidekiq6' do
35
+ gem 'sidekiq', '~> 6.0'
36
+ end
37
+ end
38
+
39
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.7.0')
40
+ appraise 'rails61' do
41
+ gem 'rails', '~> 6.1.4.4'
42
+ end
43
+
44
+ appraise 'rails70' do
45
+ gem 'rails', '~> 7.0.0'
46
+ end
31
47
  end
32
48
 
33
49
  appraise 'sidekiq51' do
data/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/).
6
6
 
7
+ ## [2.6.0] - 2021-12-22
8
+ ### Added
9
+ - Allow using the gem with rails 7
10
+ - Add a build for ruby 3.0
11
+
12
+ ### Removed
13
+ - Remove build for ruby 2.3
14
+
15
+ ## [2.5.4] - 2021-03-24
16
+ ### Fixed
17
+ - Remove state from Rack middleware, to prevent race conditions where one request would overwrite the state of another
18
+
19
+ ## [2.5.3] - 2020-10-27
20
+ ### Fixed
21
+ - Support for sidekiq 6 - previous versions were causing sidekiq to crash with `Internal exception!`
22
+
23
+ ## [2.5.2] - 2020-10-21
24
+ ### Fixed
25
+ - Support for sidekiq 6
26
+
7
27
  ## [2.5.1] - 2020-01-02
8
28
  ### Fixed
9
29
  - Fixed a long standing bug that would mask exceptions raised by the host application when serving requests. The original exception would be replaced with a `TypeError` one due to a HTTP status code not being available within `Loga::Rack::Logger`.
data/Gemfile CHANGED
@@ -4,5 +4,5 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  group :test do
7
- gem 'simplecov'
7
+ gem 'simplecov', '~> 0.17.0'
8
8
  end
data/Guardfile CHANGED
@@ -15,7 +15,7 @@ end
15
15
 
16
16
  group :rails do
17
17
  %w[production development].each do |env|
18
- %w[rails32 rails40 rails50 rails52].each do |appraisal|
18
+ %w[rails52 rails61 rails70].each do |appraisal|
19
19
  cmd = "RACK_ENV=#{env} bundle exec appraisal #{appraisal} rspec"
20
20
 
21
21
  guard :rspec, all_on_start: true, cmd: cmd do
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![Build Status](https://circleci.com/gh/FundingCircle/loga/tree/master.svg?style=shield&circle-token=9b81c3cf8468a8c3dc760f4c0398cf8914cb27d4)](https://circleci.com/gh/FundingCircle/loga/tree/master)
5
5
  [![Code Quality](https://codeclimate.com/repos/5563694f6956805723005d2f/badges/8eecb9144730614fb39e/gpa.svg)](https://codeclimate.com/repos/5563694f6956805723005d2f/feed)
6
6
  [![Test Coverage](https://codeclimate.com/repos/5563694f6956805723005d2f/badges/8eecb9144730614fb39e/coverage.svg)](https://codeclimate.com/repos/5563694f6956805723005d2f/coverage)
7
- [![Dependency Status](https://gemnasium.com/badges/github.com/FundingCircle/loga.svg)](https://gemnasium.com/github.com/FundingCircle/loga)
7
+ [![Dependency Status](https://img.shields.io/librariesio/release/rubygems/loga)](https://libraries.io/rubygems/loga)
8
8
 
9
9
  ## Description
10
10
 
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gem "rails", "~> 6.0.0"
6
6
 
7
7
  group :test do
8
- gem "simplecov"
8
+ gem "simplecov", "~> 0.17.0"
9
9
  end
10
10
 
11
11
  gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 6.1.4.4"
6
+
7
+ group :test do
8
+ gem "simplecov", "~> 0.17.0"
9
+ end
10
+
11
+ gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.0.0"
6
+
7
+ group :test do
8
+ gem "simplecov", "~> 0.17.0"
9
+ end
10
+
11
+ gemspec path: "../"
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gem "sidekiq", "~> 5.1.0"
6
6
 
7
7
  group :test do
8
- gem "simplecov"
8
+ gem "simplecov", "~> 0.17.0"
9
9
  end
10
10
 
11
11
  gemspec path: "../"
@@ -0,0 +1,11 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "sidekiq", "~> 6.0"
6
+
7
+ group :test do
8
+ gem "simplecov", "~> 0.17.0"
9
+ end
10
+
11
+ gemspec path: "../"
@@ -5,7 +5,7 @@ source "https://rubygems.org"
5
5
  gem "sinatra", "~> 1.4.0"
6
6
 
7
7
  group :test do
8
- gem "simplecov"
8
+ gem "simplecov", "~> 0.17.0"
9
9
  end
10
10
 
11
11
  gemspec path: "../"
@@ -3,7 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  group :test do
6
- gem "simplecov"
6
+ gem "simplecov", "~> 0.17.0"
7
7
  end
8
8
 
9
9
  gemspec path: "../"
@@ -79,6 +79,7 @@ module Loga
79
79
  { format: ENV['LOGA_FORMAT'].presence }.reject { |_, v| v.nil? }
80
80
  end
81
81
 
82
+ # Note: sidekiq 6 will extend the logger -> https://github.com/mperham/sidekiq/blob/v6.1.2/lib/sidekiq.rb#L210
82
83
  def initialize_logger
83
84
  device.sync = sync
84
85
  logger = Logger.new(device)
@@ -47,7 +47,8 @@ module Loga
47
47
  end
48
48
 
49
49
  event.timestamp ||= time
50
- event.data ||= {}
50
+ # Overwrite sidekiq_context data anything manually specified
51
+ event.data = sidekiq_context.merge!(event.data || {})
51
52
  event.data.tap do |hash|
52
53
  hash.merge! compute_exception(event.exception)
53
54
  hash.merge! compute_type(event.type)
@@ -101,6 +102,20 @@ module Loga
101
102
  tags: current_tags.join(' '),
102
103
  }
103
104
  end
105
+
106
+ def sidekiq_context
107
+ return {} unless defined?(::Sidekiq::Context)
108
+
109
+ c = ::Sidekiq::Context.current
110
+
111
+ # The context usually holds :class and :jid. :elapsed is added when the job ends
112
+ data = c.dup
113
+ if data.key?(:elapsed)
114
+ data[:duration] = data[:elapsed].to_f
115
+ data.delete(:elapsed)
116
+ end
117
+ data
118
+ end
104
119
  end
105
120
  end
106
121
  end
@@ -7,65 +7,57 @@ module Loga
7
7
  @app = app
8
8
  end
9
9
 
10
- def call(env)
10
+ def call(env, started_at = Time.now)
11
11
  request = Loga::Rack::Request.new(env)
12
12
  env['loga.request.original_path'] = request.path
13
13
 
14
14
  if logger.respond_to?(:tagged)
15
- logger.tagged(compute_tags(request)) { call_app(request, env) }
15
+ logger.tagged(compute_tags(request)) { call_app(request, env, started_at) }
16
16
  else
17
- call_app(request, env)
17
+ call_app(request, env, started_at)
18
18
  end
19
19
  end
20
20
 
21
21
  private
22
22
 
23
- attr_reader :data, :env, :request, :started_at
24
-
25
- def call_app(request, env)
26
- @data = {}
27
- @env = env
28
- @request = request
29
- @started_at = Time.now
30
-
31
- @app.call(env).tap { |status, _headers, _body| data['status'] = status.to_i }
23
+ def call_app(request, env, started_at)
24
+ status, _headers, _body = @app.call(env)
32
25
  ensure
33
- set_data
34
- send_message
35
- end
26
+ data = generate_data(request, status, started_at)
36
27
 
37
- # rubocop:disable Metrics/LineLength
38
- def set_data
39
- data['method'] = request.request_method
40
- data['path'] = request.original_path
41
- data['params'] = request.filtered_parameters
42
- data['request_id'] = request.uuid
43
- data['request_ip'] = request.ip
44
- data['user_agent'] = request.user_agent
45
- data['controller'] = request.controller_action_name if request.controller_action_name
46
- data['duration'] = duration_in_ms(started_at, Time.now)
47
-
48
- # If data['status'] is nil we assume an exception was raised when calling the application
49
- data['status'] ||= 500
50
- end
51
- # rubocop:enable Metrics/LineLength
28
+ exception = compute_exception(env)
52
29
 
53
- def send_message
54
30
  event = Loga::Event.new(
55
31
  data: { request: data },
56
- exception: compute_exception,
57
- message: compute_message,
32
+ exception: exception,
33
+ message: compute_message(request, data),
58
34
  timestamp: started_at,
59
35
  type: 'request',
60
36
  )
61
- logger.public_send(compute_level, event)
37
+
38
+ exception ? logger.error(event) : logger.info(event)
39
+ end
40
+
41
+ def generate_data(request, status, started_at)
42
+ controller = request.controller_action_name
43
+ status ||= 500
44
+ {
45
+ 'method' => request.request_method,
46
+ 'path' => request.original_path,
47
+ 'params' => request.filtered_parameters,
48
+ 'request_id' => request.uuid,
49
+ 'request_ip' => request.ip,
50
+ 'user_agent' => request.user_agent,
51
+ 'duration' => duration_in_ms(started_at, Time.now),
52
+ 'status' => status.to_i,
53
+ }.tap { |d| d['controller'] = controller if controller }
62
54
  end
63
55
 
64
56
  def logger
65
57
  Loga.logger
66
58
  end
67
59
 
68
- def compute_message
60
+ def compute_message(request, data)
69
61
  '%<method>s %<filtered_full_path>s %<status>d in %<duration>dms' % {
70
62
  method: request.request_method,
71
63
  filtered_full_path: request.filtered_full_path,
@@ -74,19 +66,14 @@ module Loga
74
66
  }
75
67
  end
76
68
 
77
- def compute_level
78
- compute_exception ? :error : :info
79
- end
69
+ def compute_exception(env)
70
+ exception =
71
+ env['loga.exception'] || env['action_dispatch.exception'] ||
72
+ env['sinatra.error'] || env['rack.exception']
80
73
 
81
- def compute_exception
82
74
  filter_exceptions.include?(exception.class.to_s) ? nil : exception
83
75
  end
84
76
 
85
- def exception
86
- env['loga.exception'] || env['action_dispatch.exception'] ||
87
- env['sinatra.error'] || env['rack.exception']
88
- end
89
-
90
77
  def filter_exceptions
91
78
  Loga.configuration.filter_exceptions
92
79
  end
data/lib/loga/railtie.rb CHANGED
@@ -125,7 +125,7 @@ module Loga
125
125
  def silence_rails_rack_logger
126
126
  case Rails::VERSION::MAJOR
127
127
  when 3 then require 'loga/ext/rails/rack/logger3.rb'
128
- when 4..6 then require 'loga/ext/rails/rack/logger.rb'
128
+ when 4..7 then require 'loga/ext/rails/rack/logger.rb'
129
129
  else
130
130
  raise Loga::ConfigurationError,
131
131
  "Rails #{Rails::VERSION::MAJOR} is unsupported"
data/lib/loga/sidekiq.rb CHANGED
@@ -1,16 +1,24 @@
1
- require 'loga/sidekiq/job_logger'
2
-
3
1
  module Loga
4
2
  module Sidekiq
5
3
  def self.configure_logging
6
4
  return unless defined?(::Sidekiq)
7
5
  return if Gem::Version.new(::Sidekiq::VERSION) < Gem::Version.new('5.0')
8
6
 
9
- ::Sidekiq.configure_server do |config|
10
- config.options[:job_logger] = Loga::Sidekiq::JobLogger
7
+ if Gem::Version.new(::Sidekiq::VERSION) < Gem::Version.new('6.0')
8
+ require 'loga/sidekiq5/job_logger'
9
+
10
+ ::Sidekiq.configure_server do |config|
11
+ config.options[:job_logger] = Loga::Sidekiq5::JobLogger
12
+ end
13
+ elsif Gem::Version.new(::Sidekiq::VERSION) < Gem::Version.new('7.0')
14
+ require 'loga/sidekiq6/job_logger'
15
+
16
+ ::Sidekiq.configure_server do |config|
17
+ config.options[:job_logger] = Loga::Sidekiq6::JobLogger
18
+ end
11
19
  end
12
20
 
13
- ::Sidekiq::Logging.logger = Loga.configuration.logger
21
+ ::Sidekiq.logger = Loga.configuration.logger
14
22
  end
15
23
  end
16
24
  end
@@ -1,9 +1,7 @@
1
1
  module Loga
2
- module Sidekiq
2
+ module Sidekiq5
3
3
  # The approach of using a custom job logger in sidekiq was introduced
4
4
  # in v5.0: https://github.com/mperham/sidekiq/pull/3235
5
- # This job logger does not support logging for Sidekiq versions
6
- # that are before 5.0
7
5
  class JobLogger
8
6
  include Loga::Utilities
9
7
 
@@ -0,0 +1,50 @@
1
+ require 'sidekiq/job_logger'
2
+
3
+ module Loga
4
+ module Sidekiq6
5
+ class JobLogger < ::Sidekiq::JobLogger
6
+ EVENT_TYPE = 'sidekiq'.freeze
7
+
8
+ def call(item, _queue)
9
+ start = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
10
+
11
+ yield
12
+
13
+ with_elapsed_time_context(start) do
14
+ loga_log(
15
+ message: "#{item['class']} with jid: '#{item['jid']}' done", item: item,
16
+ )
17
+ end
18
+ rescue Exception => e # rubocop:disable Lint/RescueException
19
+ with_elapsed_time_context(start) do
20
+ loga_log(
21
+ message: "#{item['class']} with jid: '#{item['jid']}' fail", item: item,
22
+ exception: e
23
+ )
24
+ end
25
+
26
+ raise
27
+ end
28
+
29
+ private
30
+
31
+ def loga_log(message:, item:, exception: nil)
32
+ data = {
33
+ 'created_at' => item['created_at'],
34
+ 'enqueued_at' => item['enqueued_at'],
35
+ 'jid' => item['jid'],
36
+ 'queue' => item['queue'],
37
+ 'retry' => item['retry'],
38
+ 'params' => item['args'],
39
+ 'class' => item['class'],
40
+ }
41
+ if exception
42
+ data['exception'] = exception
43
+ @logger.warn(Event.new(type: EVENT_TYPE, message: message, data: data))
44
+ else
45
+ @logger.info(Event.new(type: EVENT_TYPE, message: message, data: data))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/loga/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Loga
2
- VERSION = '2.5.1'.freeze
2
+ VERSION = '2.6.0'.freeze
3
3
  end
data/loga.gemspec CHANGED
@@ -21,17 +21,17 @@ Gem::Specification.new do |spec|
21
21
  spec.add_dependency 'activesupport', '>= 2.3.8'
22
22
  spec.add_dependency 'rack'
23
23
 
24
- spec.add_development_dependency 'appraisal', '~> 2.2.0'
25
- spec.add_development_dependency 'bundler', '~> 1.6'
24
+ spec.add_development_dependency 'appraisal', '~> 2.4'
25
+ spec.add_development_dependency 'bundler', '>= 1.6'
26
26
  spec.add_development_dependency 'byebug'
27
27
  spec.add_development_dependency 'guard', '~> 2.13'
28
- spec.add_development_dependency 'guard-rspec', '~> 4.7.3'
28
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
29
29
  spec.add_development_dependency 'guard-rubocop', '~> 1.2'
30
30
  spec.add_development_dependency 'pry'
31
31
  spec.add_development_dependency 'rack-test'
32
32
  spec.add_development_dependency 'rake'
33
33
  spec.add_development_dependency 'fakeredis'
34
- spec.add_development_dependency 'rspec', '~> 3.7.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.7'
35
35
  spec.add_development_dependency 'rubocop', '~> 0.57.0'
36
36
  spec.add_development_dependency 'rubocop-rspec'
37
37
  spec.add_development_dependency 'timecop'