sapience 1.0.3 → 1.0.4

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +12 -4
  4. data/Rakefile +1 -1
  5. data/dev-entrypoint.sh +5 -0
  6. data/docker-compose.yml +10 -1
  7. data/lib/sapience/appender/datadog.rb +20 -29
  8. data/lib/sapience/configuration.rb +2 -2
  9. data/lib/sapience/extensions/action_controller/notifications.rb +54 -0
  10. data/lib/sapience/extensions/active_job/notifications.rb +35 -0
  11. data/lib/sapience/extensions/active_record/notifications.rb +45 -0
  12. data/lib/sapience/extensions/grape/middleware/logging.rb +1 -1
  13. data/lib/sapience/extensions/grape/notifications.rb +46 -0
  14. data/lib/sapience/extensions/notifications.rb +32 -0
  15. data/lib/sapience/grape.rb +2 -0
  16. data/lib/sapience/rails/engine.rb +13 -2
  17. data/lib/sapience/sapience.rb +2 -12
  18. data/lib/sapience/version.rb +1 -1
  19. data/lib/sapience.rb +1 -0
  20. data/test_apps/grape/Gemfile +2 -1
  21. data/test_apps/grape/gemfiles/grape_0.16.2.gemfile.lock +1 -1
  22. data/test_apps/grape/gemfiles/grape_0.17.0.gemfile +1 -0
  23. data/test_apps/grape/gemfiles/grape_0.17.0.gemfile.lock +2 -1
  24. data/test_apps/grape/lib/ping/api.rb +1 -0
  25. data/test_apps/grape/spec/lib/ping/api_spec.rb +16 -2
  26. data/test_apps/grape/spec/spec_helper.rb +1 -0
  27. data/test_apps/rails/Gemfile +1 -1
  28. data/test_apps/rails/app/jobs/test_job.rb +12 -0
  29. data/test_apps/rails/app/models/post.rb +1 -1
  30. data/test_apps/rails/app/models/user.rb +1 -0
  31. data/test_apps/rails/app/workers/test_worker.rb +1 -1
  32. data/test_apps/rails/config/database.yml +6 -7
  33. data/test_apps/rails/config/initializers/sneakers.rb +9 -10
  34. data/test_apps/rails/db/migrate/20160902141445_create_posts.rb +7 -1
  35. data/test_apps/rails/db/schema.rb +7 -3
  36. data/test_apps/rails/lib/external_sneaker.rb +4 -4
  37. data/test_apps/rails/lib/tasks/sneakers.rake +21 -0
  38. data/test_apps/rails/spec/controllers/posts_controller_spec.rb +1 -1
  39. data/test_apps/rails/spec/fixtures/sapience.yml +1 -0
  40. data/test_apps/rails/spec/jobs/test_job_spec.rb +36 -0
  41. data/test_apps/rails/spec/models/post_spec.rb +10 -1
  42. data/test_apps/rails/spec/rails_helper.rb +4 -1
  43. data/test_apps/rails/spec/requests/posts_spec.rb +37 -4
  44. data/test_apps/rails/spec/workers/test_worker_spec.rb +10 -5
  45. metadata +10 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11cce05eedf1219a30f6da98358d1047be347bb5
4
- data.tar.gz: 439781b493d904279964f6b6dc74caea43058082
3
+ metadata.gz: 67aad6878ced498fd8bb3f53f38316eb1d0c311a
4
+ data.tar.gz: 8530772e63086bd9e30d1bcba0a743e3f2df83ec
5
5
  SHA512:
6
- metadata.gz: 702c8f1c1af9e49c5a2f0b3f352c4f2d0835836952e420e3e827a105b586723041605ff76ef29b1ffb1eea351a466faeabc79fa29be639e890b31b3a3581a1aa
7
- data.tar.gz: 750f844879a9d7b5d886bc5e3fdc2a7e7aa8cf8b33487e3b090494d0b71c7b3d55b86ab2b36435abe5dbad838b8bd85a4adc028389b6b0b04afc71a78556cfb6
6
+ metadata.gz: 3ea440554539370aa76691e74c66d8e1edc4ebdab2b6539250206302892ca8d699508f0255d35c2e2b455dfa03d4af189c7f10da718be3da32bb27ed94653b93
7
+ data.tar.gz: a091be856a79a94b3a62a5b4c0e90456f89f68f9afab0e41c386a5663849cd98ca16e526a62d904c7394d69cd5c65c19d1d7bc21319f76c4a674fe9b22cf4d8a
data/.gitignore CHANGED
@@ -11,3 +11,5 @@ log/
11
11
 
12
12
  **/examples.txt
13
13
  **/*.gem
14
+
15
+ /vendor/
data/.travis.yml CHANGED
@@ -1,16 +1,24 @@
1
1
  sudo: required
2
2
  dist: trusty
3
3
  language: ruby
4
+ cache:
5
+ bundler: true
6
+ directories:
7
+ - $HOME/docker
8
+
4
9
  services:
5
10
  - docker
6
11
 
7
12
  before_script:
8
13
  - mkdir -p coverage/rails coverage/grape coverage/sapience
14
+ - bundle exec rake reevoocop
15
+
16
+ env:
17
+ - TEST_SUITE=rspec INSTALL=false
18
+ - TEST_SUITE=rails INSTALL=false
19
+ - TEST_SUITE=grape INSTALL=false
9
20
 
10
- script:
11
- - docker-compose run rspec
12
- - docker-compose run rails
13
- - docker-compose run grape
21
+ script: "docker-compose run $TEST_SUITE"
14
22
 
15
23
  after_success:
16
24
  - chown -R $(whoami) coverage
data/Rakefile CHANGED
@@ -12,4 +12,4 @@ begin
12
12
  rescue LoadError # rubocop:disable Lint/HandleExceptions
13
13
  end
14
14
 
15
- task default: [:reevoocop, :spec]
15
+ task default: [:spec]
data/dev-entrypoint.sh CHANGED
@@ -7,4 +7,9 @@ set -e
7
7
 
8
8
  # 5: Check or install the app dependencies via Bundler:
9
9
  bundle check || bundle install --jobs 8 --retry 5
10
+
11
+ if [ -f config/database.yml ]; then
12
+ bundle exec rake db:create db:migrate db:test:prepare
13
+ fi;
14
+
10
15
  exec "$@"
data/docker-compose.yml CHANGED
@@ -10,7 +10,11 @@ services:
10
10
  RABBITMQ_DEFAULT_USER: sapience
11
11
  RABBITMQ_DEFAULT_PASS: tests
12
12
 
13
-
13
+ postgres:
14
+ image: postgres:latest
15
+ environment:
16
+ POSTGRES_USER: sapience
17
+ POSTGRES_PASSWORD: tests
14
18
 
15
19
  base:
16
20
  image: ruby:2.3
@@ -26,6 +30,7 @@ services:
26
30
  # Enable sending signals (CTRL+C, CTRL+P + CTRL+Q) into the container:
27
31
  tty: true
28
32
  volumes:
33
+ # - vendor/bundle:/usr/local/bundle
29
34
  # Mount our app code directory (".") into our app containers at the
30
35
  # "/usr/src/app" folder:
31
36
  - .:/usr/src/app
@@ -55,8 +60,12 @@ services:
55
60
  command: bundle exec rspec
56
61
  depends_on:
57
62
  - rabbitmq
63
+ - postgres
58
64
  environment:
59
65
  APP_NAME: rails_app
66
+ POSTGRES_HOST: postgres
67
+ POSTGRES_USER: sapience
68
+ POSTGRES_PASSWORD: tests
60
69
  AMQP: amqp://sapience:tests@rabbitmq:5672
61
70
  PATH: /usr/src/app/bin:/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
62
71
 
@@ -33,7 +33,6 @@ module Sapience
33
33
  @tags = options.delete(:tags)
34
34
  @uri = URI.parse(url)
35
35
  fail('Statsd only supports udp. Example: "udp://localhost:8125"') if @uri.scheme != "udp"
36
-
37
36
  super(options, &block)
38
37
  end
39
38
 
@@ -47,58 +46,50 @@ module Sapience
47
46
  return false unless metric
48
47
 
49
48
  if log.duration
50
- timing(metric, log.duration)
49
+ timing(metric, log.duration, tags: log.tags)
51
50
  else
52
51
  amount = (log.metric_amount || 1).round
53
- if amount < 0
54
- decrement(metric, amount.abs)
55
- else
56
- increment(metric, amount)
57
- end
52
+ count(metric, amount, tags: log.tags)
58
53
  end
59
54
  true
60
55
  end
61
56
 
62
- def timing(metric, duration = 0)
57
+ def timing(metric, duration = 0, options = {})
63
58
  if block_given?
64
59
  start = Time.now
65
60
  yield
66
- provider.timing(metric, ((Time.now - start) * 1000).floor)
61
+ provider.timing(metric, ((Time.now - start) * 1000).floor, options)
67
62
  else
68
- provider.timing(metric, duration)
63
+ provider.timing(metric, duration, options)
69
64
  end
70
65
  end
71
66
 
72
- def increment(metric, amount = 1)
73
- provider.batch do
74
- amount.times { provider.increment(metric) }
75
- end
67
+ def increment(metric, options = {})
68
+ provider.increment(metric, options)
76
69
  end
77
70
 
78
- def decrement(metric, amount = 1)
79
- provider.batch do
80
- amount.times { provider.decrement(metric) }
81
- end
71
+ def decrement(metric, options = {})
72
+ provider.decrement(metric, options)
82
73
  end
83
74
 
84
- def histogram(metric, amount)
85
- provider.histogram(metric, amount)
75
+ def histogram(metric, amount, options = {})
76
+ provider.histogram(metric, amount, options)
86
77
  end
87
78
 
88
- def gauge(metric, amount, hash = {})
89
- provider.gauge(metric, amount, hash)
79
+ def gauge(metric, amount, options = {})
80
+ provider.gauge(metric, amount, options)
90
81
  end
91
82
 
92
- def count(metric, amount, hash = {})
93
- provider.count(metric, amount, hash)
83
+ def count(metric, amount, options = {})
84
+ provider.count(metric, amount, options)
94
85
  end
95
86
 
96
- def time(metric, &block)
97
- provider.time(metric, &block)
87
+ def time(metric, options = {}, &block)
88
+ provider.time(metric, options, &block)
98
89
  end
99
90
 
100
- def batch
101
- yield provider
91
+ def batch(&block)
92
+ provider.batch(&block)
102
93
  end
103
94
 
104
95
  def event(title, text, options = {})
@@ -106,7 +97,7 @@ module Sapience
106
97
  end
107
98
 
108
99
  def namespace
109
- ns = Sapience.namify(Sapience.app_name)
100
+ ns = Sapience.namify(app_name)
110
101
  ns << ".#{Sapience.namify(Sapience.environment)}" if Sapience.environment
111
102
  ns
112
103
  end
@@ -21,8 +21,8 @@ module Sapience
21
21
 
22
22
  # Initial default Level for all new instances of Sapience::Logger
23
23
  def initialize(options = {}) # rubocop:disable AbcSize
24
- fail ArgumentError, "options need to be a hash" unless options.is_a?(Hash)
25
- @options = DEFAULT.merge(options.deep_symbolize_keyz!)
24
+ fail ArgumentError, "options need to be a hash #{options.inspect}" unless options.is_a?(Hash)
25
+ @options = DEFAULT.merge(options.dup.deep_symbolize_keyz!)
26
26
  @options[:log_executor] &&= @options[:log_executor].to_sym
27
27
  validate_log_executor!(@options[:log_executor])
28
28
  self.default_level = @options[:log_level].to_sym
@@ -0,0 +1,54 @@
1
+ module Sapience
2
+ module Extensions
3
+ module ActionController
4
+ class Notifications < ::Sapience::Extensions::Notifications
5
+ # Options:
6
+ #
7
+ # *<tt>:metric_name</tt> - the metric name, defaults to "rails.request"
8
+ # *<tt>:tags</tt> - additional tags
9
+ def initialize(options = {})
10
+ @metric_name = options[:metric_name] || "rails.request"
11
+ super
12
+ Sapience::Extensions::Notifications.subscribe("process_action.action_controller") do |event|
13
+ record event
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def record(event) # rubocop:disable AbcSize
20
+ return unless record?
21
+
22
+ payload = event.payload
23
+ method = payload[:method].downcase
24
+ status = payload[:status]
25
+ action = payload[:action]
26
+ ctrl = payload[:controller].sub(/Controller$/, "").underscore
27
+ format = payload[:format]
28
+
29
+ tags = self.tags + %W(
30
+ method:#{method}
31
+ status:#{status}
32
+ action:#{action}
33
+ controller:#{ctrl}
34
+ format:#{format}
35
+ )
36
+
37
+ metrics.batch do
38
+ metrics.increment metric_name, tags: tags
39
+ metrics.timing("#{metric_name}.time", event.duration, tags: tags)
40
+ if payload[:db_runtime]
41
+ metrics.timing("#{metric_name}.time.db", payload[:db_runtime].round(10), tags: tags)
42
+ end
43
+
44
+
45
+
46
+ if payload[:view_runtime]
47
+ metrics.timing("#{metric_name}.time.view", payload[:view_runtime].round(2), tags: tags)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ module Sapience
2
+ module Extensions
3
+ module ActiveJob
4
+ class Notifications < ::Sapience::Extensions::Notifications
5
+
6
+ # Options:
7
+ #
8
+ # *<tt>:metric_name</tt> - the metric name, defaults to "activejob.perform"
9
+ # *<tt>:tags</tt> - additional tags
10
+ def initialize(opts = {})
11
+ super
12
+ @metric_name = opts[:metric_name] || "activejob.perform"
13
+
14
+ Sapience::Extensions::Notifications.subscribe "perform.active_job" do |event|
15
+ record event
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def record(event) # rubocop:disable AbcSize
22
+ return unless record?
23
+
24
+ job = event.payload[:job]
25
+ name = job.class.name.sub(/Job$/, "").underscore
26
+ tags = self.tags + %W(name:#{name} queue:#{job.queue_name})
27
+ metrics.batch do
28
+ metrics.increment metric_name, tags: tags
29
+ metrics.timing "#{metric_name}.time", event.duration, tags: tags
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ module Sapience
2
+ module Extensions
3
+ module ActiveRecord
4
+ class Notifications < ::Sapience::Extensions::Notifications
5
+ # Options:
6
+ #
7
+ # *<tt>:metric_name</tt> - the metric name, defaults to "activerecord.query"
8
+ # *<tt>:include_schema</tt> - record schema queries, off by default
9
+ # *<tt>:include_generic</tt> - record general (nameless) queries, off by default
10
+ # *<tt>:tags</tt> - additional tags
11
+ def initialize(opts = {})
12
+ super
13
+ @metric_name = opts[:metric_name] || "activerecord.sql"
14
+ @include_schema = opts[:include_schema] == true
15
+ @include_generic = opts[:include_generic] == true
16
+ @include_raw = opts[:include_raw] == true
17
+
18
+ Sapience::Extensions::Notifications.subscribe "sql.active_record" do |event|
19
+ record event
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def record(event) # rubocop:disable AbcSize, CyclomaticComplexity, PerceivedComplexity
26
+ return unless record?
27
+
28
+ payload = event.payload
29
+ name = payload[:name]
30
+ return if (name.nil? || name == "SQL") && !@include_generic
31
+ return if name == "SCHEMA" && !@include_schema
32
+
33
+ name = name.downcase.split(/\W/).join(".") if name
34
+ tags = self.tags.dup
35
+ tags.push "query:#{name}" if name
36
+
37
+ metrics.batch do
38
+ metrics.increment metric_name, tags: tags
39
+ metrics.timing "#{metric_name}.time", event.duration, tags: tags
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -39,7 +39,7 @@ module Sapience
39
39
  {
40
40
  method: request.request_method,
41
41
  request_path: request.path,
42
- format: "json",
42
+ format: env["api.endpoint"].instance_variable_get(:@app).instance_variable_get(:@app).options[:format],
43
43
  status: response.try(:status) || 404,
44
44
  class_name: env["api.endpoint"].options[:for].to_s,
45
45
  action: "index",
@@ -0,0 +1,46 @@
1
+ module Sapience
2
+ module Extensions
3
+ module Grape
4
+ class Notifications < ::Sapience::Extensions::Notifications
5
+ # Options:
6
+ #
7
+ # *<tt>:metric_name</tt> - the metric name, defaults to "grape.request"
8
+ # *<tt>:tags</tt> - additional tags
9
+ def initialize(opts = {})
10
+ super
11
+ @metric_name = opts[:metric_name] || "grape.request"
12
+
13
+ Sapience::Extensions::Notifications.subscribe "endpoint_run.grape" do |event|
14
+ record event
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def record(event) # rubocop:disable AbcSize
21
+ return unless record?
22
+
23
+ payload = event.payload
24
+ endpoint = payload[:endpoint]
25
+ route = endpoint.route
26
+ version = route.version
27
+ method = route.request_method.downcase
28
+ format = payload[:endpoint].instance_variable_get(:@app).instance_variable_get(:@app).options[:format]
29
+ path = route.pattern.path.dup
30
+
31
+ path.sub!(/\(\.#{format}\)$/, "")
32
+ path.sub!(":version/", "") if version
33
+ path.gsub!(/:(\w+)/) { |m| m[1..-1].upcase }
34
+ path.gsub!(/[^\w\/\-]+/, "_")
35
+
36
+ tags = self.tags + %W(method:#{method} format:#{format} path:#{path} status:#{endpoint.status})
37
+ tags.push "version:#{version}" if version
38
+ metrics.batch do
39
+ metrics.increment metric_name, tags: tags
40
+ metrics.timing "#{metric_name}.time", event.duration, tags: tags
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,32 @@
1
+ require "active_support"
2
+ require "active_support/notifications"
3
+
4
+ module Sapience
5
+ module Extensions
6
+ class Notifications
7
+ attr_reader :tags, :metric_name
8
+
9
+ def self.use(options = {})
10
+ new(options)
11
+ end
12
+
13
+ def self.subscribe(pattern, &block)
14
+ ::ActiveSupport::Notifications.subscribe(pattern) do |*args|
15
+ block.call ::ActiveSupport::Notifications::Event.new(*args)
16
+ end
17
+ end
18
+
19
+ def initialize(options = {})
20
+ @tags = options[:tags] || []
21
+ end
22
+
23
+ def record?
24
+ !metrics.nil?
25
+ end
26
+
27
+ def metrics
28
+ Sapience.metrics
29
+ end
30
+ end
31
+ end
32
+ end
@@ -1,6 +1,7 @@
1
1
  require "sapience"
2
2
  require "sapience/extensions/grape/timings"
3
3
  require "sapience/extensions/grape/middleware/logging"
4
+ require "sapience/extensions/grape/notifications"
4
5
 
5
6
  module Grape
6
7
  class API
@@ -12,5 +13,6 @@ module Sapience
12
13
  class Grape
13
14
  Sapience.configure
14
15
  ::Grape::API.send(:include, Sapience::Loggable)
16
+ Sapience::Extensions::Grape::Notifications.use
15
17
  end
16
18
  end
@@ -1,9 +1,14 @@
1
1
  require "sapience"
2
2
  require "sapience/extensions/action_controller/live" if defined?(ActionController::Live)
3
3
  require "sapience/extensions/action_controller/log_subscriber"
4
+ require "sapience/extensions/action_controller/notifications"
4
5
  require "sapience/extensions/action_dispatch/debug_exceptions"
5
6
  require "sapience/extensions/action_view/streaming_template_renderer"
6
- require "sapience/extensions/active_record/log_subscriber" if defined?(ActiveRecord)
7
+ if defined?(ActiveRecord)
8
+ require "sapience/extensions/active_record/log_subscriber"
9
+ require "sapience/extensions/active_record/notifications"
10
+ end
11
+ require "sapience/extensions/active_job/notifications" if defined?(ActiveJob)
7
12
  require "sapience/extensions/rails/rack/logger"
8
13
  require "sapience/extensions/rails/rack/logger_info_as_debug"
9
14
  require "sapience/extensions/action_view/log_subscriber"
@@ -67,9 +72,15 @@ module Sapience
67
72
  Bugsnag.configure { |config| config.logger = Sapience[Bugsnag] } if defined?(Bugsnag)
68
73
  Sapience::Extensions::ActionController::LogSubscriber.attach_to :action_controller
69
74
  # Sapience::Extensions::ActiveSupport::MailerLogSubscriber.attach_to :action_mailer
70
- Sapience::Extensions::ActiveRecord::LogSubscriber.attach_to :active_record if defined?(ActiveRecord)
75
+ if defined?(ActiveRecord)
76
+ Sapience::Extensions::ActiveRecord::LogSubscriber.attach_to :active_record
77
+ Sapience::Extensions::ActiveRecord::Notifications.use
78
+ end
79
+
71
80
  Sapience::Extensions::ActionView::LogSubscriber.attach_to :action_view
72
81
  # Sapience::Extensions::ActiveJob::LogSubscriber.attach_to :active_job
82
+ Sapience::Extensions::ActionController::Notifications.use
83
+ Sapience::Extensions::ActiveJob::Notifications.use if defined?(ActiveJob)
73
84
  end
74
85
  end
75
86
  end
@@ -33,12 +33,9 @@ module Sapience
33
33
  RAILS_ENV = "RAILS_ENV".freeze
34
34
  SAPIENCE_ENV = "SAPIENCE_ENV".freeze
35
35
 
36
- # TODO: Maybe when configuring with a block we should create a new config?
37
- # See the TODO note on .config for more information
38
36
  def self.configure(force: false)
39
37
  yield config if block_given?
40
38
  return config if configured? && force == false
41
- reload_config!
42
39
  reset_appenders!
43
40
  add_appenders(*config.appenders)
44
41
  @@configured = true
@@ -50,13 +47,6 @@ module Sapience
50
47
  @@config_hash ||= ConfigLoader.load_from_file
51
48
  end
52
49
 
53
- def self.reload_config!
54
- @@config_hash = ConfigLoader.load_from_file
55
- end
56
-
57
- # TODO: Should we really always read from file?
58
- # What if someone wants to configure sapience with a block
59
- # without reading the default.yml?
60
50
  def self.config
61
51
  @@config ||= begin
62
52
  options = config_hash[environment]
@@ -220,7 +210,7 @@ module Sapience
220
210
  # logger.debug("Login time", user: 'Joe', duration: 100, ip_address: '127.0.0.1')
221
211
  def self.add_appender(appender, options = {}, _deprecated_level = nil, &_block)
222
212
  fail ArgumentError, "options should be a hash" unless options.is_a?(Hash)
223
- options.deep_symbolize_keyz!
213
+ options = options.dup.deep_symbolize_keyz!
224
214
  appender_class = constantize_symbol(appender)
225
215
  validate_appender!(appender_class)
226
216
 
@@ -286,7 +276,7 @@ module Sapience
286
276
  end
287
277
 
288
278
  def self.metrics
289
- @@metrics ||= Sapience.add_appender(:datadog)
279
+ @@metrics ||= nil
290
280
  end
291
281
 
292
282
  def self.logger=(logger)
@@ -1,3 +1,3 @@
1
1
  module Sapience
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.4"
3
3
  end
data/lib/sapience.rb CHANGED
@@ -4,6 +4,7 @@ require "sapience/core_ext/hash"
4
4
  require "sapience/core_ext/symbol"
5
5
  require "sapience/core_ext/thread"
6
6
  require "sapience/sapience"
7
+ require "sapience/extensions/notifications"
7
8
 
8
9
  # @formatter:off
9
10
 
@@ -11,11 +11,12 @@ gem "puma", "~> 3.0"
11
11
  gem "sapience", path: "../.."
12
12
  gem "sentry-raven"
13
13
  gem "dogstatsd-ruby"
14
+ gem "activesupport"
14
15
 
15
16
  group :development, :test do
16
17
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
17
18
  gem "byebug", platform: :mri
18
- # gem "pry-nav"
19
+ # gem "pry-nav", platform: :mri
19
20
  gem "pry-byebug", platform: :mri
20
21
  gem "rspec"
21
22
  gem "racksh"
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../../..
3
3
  specs:
4
- sapience (1.0.2)
4
+ sapience (1.0.3)
5
5
  concurrent-ruby (~> 1.0)
6
6
 
7
7
  GEM
@@ -7,6 +7,7 @@ gem "puma", "~> 3.0"
7
7
  gem "sapience", :path => "../../.."
8
8
  gem "sentry-raven"
9
9
  gem "dogstatsd-ruby"
10
+ gem "activesupport"
10
11
 
11
12
  group :development, :test do
12
13
  gem "byebug", :platform => :mri
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../../..
3
3
  specs:
4
- sapience (1.0.2)
4
+ sapience (1.0.3)
5
5
  concurrent-ruby (~> 1.0)
6
6
 
7
7
  GEM
@@ -118,6 +118,7 @@ PLATFORMS
118
118
  ruby
119
119
 
120
120
  DEPENDENCIES
121
+ activesupport
121
122
  appraisal
122
123
  byebug
123
124
  dogstatsd-ruby
@@ -1,6 +1,7 @@
1
1
  require "grape"
2
2
  require "grape/api"
3
3
  require "sapience/grape"
4
+ require "active_support/notifications"
4
5
 
5
6
  module Ping
6
7
  class API < ::Grape::API
@@ -16,11 +16,25 @@ describe Ping::API do
16
16
  expect(last_response.body).to match(/PONG/)
17
17
  end
18
18
 
19
+ describe "ActiveSupport::Notifications" do
20
+ let(:metrics) { Sapience.metrics }
21
+ let(:tags) { %w(method:get format:json path:/api/ping status:200) }
22
+ before do
23
+ Sapience.configure { |c| c.app_name = "grape" }
24
+ Sapience.add_appender(:datadog)
25
+ end
26
+ specify do
27
+ expect(metrics).to receive(:increment).with("grape.request", tags: tags)
28
+ expect(metrics).to receive(:timing).with("grape.request.time", kind_of(Float), tags: tags)
29
+ get "/api/ping"
30
+ end
31
+ end
32
+
19
33
  it "logs something" do
20
34
  expect(logger).to receive(:info).with(
21
35
  method: "GET",
22
36
  request_path: "/api/ping",
23
- format: "json",
37
+ format: :json,
24
38
  status: 200,
25
39
  class_name: "Ping::API",
26
40
  action: "index",
@@ -44,7 +58,7 @@ describe Ping::API do
44
58
  expect(logger).to receive(:info).with(
45
59
  method: "GET",
46
60
  request_path: "/api/404",
47
- format: "json",
61
+ format: :json,
48
62
  status: 404,
49
63
  class_name: "Ping::API",
50
64
  action: "index",
@@ -3,6 +3,7 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
3
3
  require "sapience/grape"
4
4
  require "rspec/its"
5
5
  require "ping"
6
+ require "pry"
6
7
 
7
8
  RSpec.configure do |config|
8
9
  config.expect_with :rspec do |expectations|
@@ -4,7 +4,7 @@ source "https://rubygems.org"
4
4
  # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
5
5
  gem "rails", "~> 5.0.0"
6
6
  # Use sqlite3 as the database for Active Record
7
- gem "sqlite3"
7
+ gem "pg"
8
8
  # Use Puma as the app server
9
9
  gem "puma", "~> 3.0"
10
10
  # Use Redis adapter to run Action Cable in production
@@ -0,0 +1,12 @@
1
+ require "application_job"
2
+ require_relative "../../../../spec/support/file_helper"
3
+
4
+ class TestJob < ApplicationJob
5
+ VERIFICATION_FILE = Rails.root.join("tmp/test_job.verified")
6
+ queue_as :test_queue
7
+ include FileHelper
8
+
9
+ def perform
10
+ create_file(VERIFICATION_FILE)
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  class Post < ApplicationRecord
2
- belongs_to :author, class_name: User
2
+ belongs_to :author, class_name: User, inverse_of: :posts
3
3
  end
@@ -1,2 +1,3 @@
1
1
  class User < ApplicationRecord
2
+ has_many :posts, inverse_of: :author
2
3
  end
@@ -3,7 +3,7 @@ require_relative "../../../../spec/support/file_helper"
3
3
  class TestWorker
4
4
  QUEUE_NAME = :sneakers_queue
5
5
  ROUTING_KEY = :sneakers_routing_key
6
- VERIFICATION_FILE = "tmp/sneakers.verified".freeze
6
+ VERIFICATION_FILE = Rails.root.join("tmp/test_worker.verified")
7
7
 
8
8
  include Sneakers::Worker
9
9
  include FileHelper
@@ -5,21 +5,20 @@
5
5
  # gem 'sqlite3'
6
6
  #
7
7
  default: &default
8
- adapter: sqlite3
8
+ adapter: postgresql
9
+ host: <%= ENV.fetch('POSTGRES_HOST') { "localhost" } %>
10
+ username: <%= ENV.fetch('POSTGRES_USER') { `whoami` } %>
11
+ password: <%= ENV.fetch('POSTGRES_PASSWORD') { nil } %>
9
12
  pool: 5
10
13
  timeout: 5000
11
14
 
12
15
  development:
13
16
  <<: *default
14
- database: db/development.sqlite3
17
+ database: rails_app_dev
15
18
 
16
19
  # Warning: The database defined as "test" will be erased and
17
20
  # re-generated from your development database when you run "rake".
18
21
  # Do not set this db to the same as development or production.
19
22
  test:
20
23
  <<: *default
21
- database: db/test.sqlite3
22
-
23
- production:
24
- <<: *default
25
- database: db/production.sqlite3
24
+ database: rails_app_test
@@ -1,15 +1,14 @@
1
1
  require "serverengine"
2
2
  require "sneakers"
3
-
4
- p ENV.fetch("AMQP") { "amqp://guest:guest@localhost:5672" }
3
+ require "sapience"
5
4
 
6
5
  Sneakers.configure(
7
- amqp: ENV.fetch("AMQP") { "amqp://guest:guest@localhost:5672" },
8
- exchange_type: :direct,
9
- log: Sapience[Sneakers], # Log file
10
- exchange: "sapience", # AMQP exchange
11
- durable: false, # Is queue durable?
12
- ack: true, # Must we acknowledge?
13
- metrics: Sapience.metrics,
6
+ amqp: ENV.fetch("AMQP") { "amqp://guest:guest@localhost:5672" },
7
+ exchange_type: :direct,
8
+ log: Sapience[Sneakers], # Log file
9
+ exchange: "sapience", # AMQP exchange
10
+ durable: false, # Is queue durable?
11
+ ack: true, # Must we acknowledge?
12
+ metrics: Sapience.metrics,
13
+ heartbeat: nil,
14
14
  )
15
- Sapience.logger.level = Logger::DEBUG
@@ -3,7 +3,13 @@ class CreatePosts < ActiveRecord::Migration[5.0]
3
3
  create_table :posts do |t|
4
4
  t.string :title
5
5
  t.string :body
6
- t.belongs_to :author, foreign_key: true
6
+ t.belongs_to :author, foreign_key: {
7
+ to_table: :users,
8
+ column: :author_id,
9
+ name: :posts_author_fk,
10
+ on_delete: :cascade,
11
+ on_update: :restrict,
12
+ }
7
13
 
8
14
  t.timestamps
9
15
  end
@@ -12,13 +12,16 @@
12
12
 
13
13
  ActiveRecord::Schema.define(version: 20160902141445) do
14
14
 
15
+ # These are extensions that must be enabled in order to support this database
16
+ enable_extension "plpgsql"
17
+
15
18
  create_table "posts", force: :cascade do |t|
16
19
  t.string "title"
17
20
  t.string "body"
18
21
  t.integer "author_id"
19
22
  t.datetime "created_at", null: false
20
23
  t.datetime "updated_at", null: false
21
- t.index ["author_id"], name: "index_posts_on_author_id"
24
+ t.index ["author_id"], name: "index_posts_on_author_id", using: :btree
22
25
  end
23
26
 
24
27
  create_table "users", force: :cascade do |t|
@@ -26,8 +29,9 @@ ActiveRecord::Schema.define(version: 20160902141445) do
26
29
  t.string "email"
27
30
  t.datetime "created_at", null: false
28
31
  t.datetime "updated_at", null: false
29
- t.index ["email"], name: "users_email_key", unique: true
30
- t.index ["username"], name: "users_username_key", unique: true
32
+ t.index ["email"], name: "users_email_key", unique: true, using: :btree
33
+ t.index ["username"], name: "users_username_key", unique: true, using: :btree
31
34
  end
32
35
 
36
+ add_foreign_key "posts", "users", column: "author_id", name: "posts_author_fk", on_update: :restrict, on_delete: :cascade
33
37
  end
@@ -1,11 +1,11 @@
1
1
  require "timeout"
2
2
 
3
3
  class ExternalSneaker
4
- attr_accessor :worker_pid, :start_command
4
+ attr_accessor :worker_pid, :start_command, :workers
5
5
 
6
- def initialize(start_command)
6
+ def initialize(start_command, *workers)
7
7
  fail ArgumentError, "start_command was expected" if start_command.nil?
8
-
8
+ self.workers = workers.map(&:to_s).join(",")
9
9
  self.start_command = start_command
10
10
  end
11
11
 
@@ -23,7 +23,7 @@ class ExternalSneaker
23
23
  private
24
24
 
25
25
  def start_child
26
- exec({ "RAILS_ENV" => Rails.env, "WORKERS" => "TestWorker" }, start_command)
26
+ exec({ "RAILS_ENV" => Rails.env, "WORKERS" => workers }, start_command)
27
27
  end
28
28
 
29
29
  def stop_child # rubocop:disable AbcSize
@@ -0,0 +1,21 @@
1
+ require "sneakers/runner"
2
+
3
+ task :environment
4
+
5
+ namespace :sneakers do
6
+ desc "Start processing jobs with all workers"
7
+ task work: :environment do
8
+ silence_warnings do
9
+ Rails.application.eager_load! unless Rails.application.config.eager_load
10
+ end
11
+
12
+ workers = ApplicationJob.subclasses.map do |klass|
13
+ klass.const_set("Wrapper", Class.new(ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper) do
14
+ from_queue klass.queue_name
15
+ end)
16
+ end
17
+
18
+
19
+ Sneakers::Runner.new(workers).run
20
+ end
21
+ end
@@ -1,6 +1,6 @@
1
1
  require "rails_helper"
2
2
 
3
- RSpec.describe PostsController, type: :controller do
3
+ describe PostsController, type: :controller do
4
4
  specify do
5
5
  expect(subject.logger).to be_a(Sapience::Logger)
6
6
  end
@@ -7,6 +7,7 @@ default:
7
7
  formatter: default
8
8
 
9
9
  test:
10
+ app_name: rails_app
10
11
  log_level: fatal
11
12
  appenders:
12
13
  - stream:
@@ -0,0 +1,36 @@
1
+ require "rails_helper"
2
+ require "serverengine"
3
+ require "sneakers"
4
+ require "sneakers/runner"
5
+ require "external_sneaker"
6
+
7
+ describe TestJob do
8
+ include FileHelper
9
+ include ActiveJob::TestHelper
10
+ let(:metrics) { Sapience.add_appender(:datadog) }
11
+ let(:tags) do
12
+ %w(name:test queue:test_queue)
13
+ end
14
+ let(:message) do
15
+ {
16
+ title: "Cool",
17
+ body: "Hot",
18
+ }
19
+ end
20
+ let(:logger) { Sapience[described_class] }
21
+
22
+ after do
23
+ delete_file("config/sapience.yml")
24
+ delete_file(described_class::VERIFICATION_FILE)
25
+ end
26
+
27
+ # TODO: Possible make this less flaky or run it with retry (rspec-retry)
28
+ it "runs properly" do
29
+ expect(metrics).to receive(:increment).with("activejob.perform", tags: tags)
30
+ expect(metrics).to receive(:timing).with("activejob.perform.time", kind_of(Float), tags: tags)
31
+
32
+ perform_enqueued_jobs do
33
+ TestJob.perform_later
34
+ end
35
+ end
36
+ end
@@ -1,5 +1,14 @@
1
1
  require "rails_helper"
2
2
 
3
3
  RSpec.describe Post, type: :model do
4
- pending "add some examples to (or delete) #{__FILE__}"
4
+ let!(:metrics) { Sapience.add_appender(:datadog, app_name: "active_record") }
5
+ let(:tags) { %w(query:post.load) }
6
+
7
+ before(:each) { create :post }
8
+
9
+ it "records som sql metrics" do
10
+ expect(metrics).to receive(:increment).with("activerecord.sql", tags: tags)
11
+ expect(metrics).to receive(:timing).with("activerecord.sql.time", kind_of(Float), tags: tags)
12
+ Post.first
13
+ end
5
14
  end
@@ -27,11 +27,14 @@ RSpec.configure do |config|
27
27
  config.infer_spec_type_from_file_location!
28
28
  config.filter_rails_from_backtrace!
29
29
  config.before(:each) do
30
- Sapience.reset!
31
30
  FileUtils.cp(
32
31
  Rails.root.join("spec/fixtures/sapience.yml"),
33
32
  Rails.root.join("config/sapience.yml"),
34
33
  )
34
+ Sapience.reset!
35
+ Sapience.configure do |c|
36
+ c.app_name = "rails_app"
37
+ end
35
38
  end
36
39
 
37
40
  config.after(:each) do
@@ -2,10 +2,43 @@ require "rails_helper"
2
2
 
3
3
  describe "Posts", type: :request do
4
4
  describe "GET /posts" do
5
- it "works! (now write some real specs)" do
6
- create :post
7
- get posts_path
8
- expect(response).to have_http_status(200)
5
+ describe "ActiveSupport::Notification" do
6
+ context "when metrics is configured" do
7
+ before do
8
+ # ActiveRecord::Base.logger = Logger.new(STDOUT)
9
+ Rails.logger.level = :debug
10
+ Sapience.add_appender(:datadog)
11
+ FactoryGirl.create_list(:post, 10)
12
+ allow_any_instance_of(Sapience::Extensions::ActiveRecord::Notifications)
13
+ .to receive(:record).and_return(true)
14
+ end
15
+
16
+ it "records a batch of metrics" do
17
+ expect(Sapience.metrics).to receive(:increment) do |metric_name, options|
18
+ expect(metric_name).to eq("rails.request")
19
+ expect(options[:tags]).to match_array(%w(method:get status:200 action:index controller:posts format:html))
20
+ end
21
+ expect(Sapience.metrics).to receive(:timing) do |metric_name, duration, options|
22
+ expect(metric_name).to eq("rails.request.time")
23
+ expect(duration).to be_a(Float).and be > 0
24
+ expect(options[:tags]).to match_array(%w(method:get status:200 action:index controller:posts format:html))
25
+ end
26
+
27
+ expect(Sapience.metrics).to receive(:timing) do |metric_name, duration, options|
28
+ expect(metric_name).to eq("rails.request.time.db")
29
+ expect(duration).to be_a(Float).and be > 0
30
+ expect(options[:tags]).to match_array(%w(method:get status:200 action:index controller:posts format:html))
31
+ end
32
+
33
+ expect(Sapience.metrics).to receive(:timing) do |metric_name, duration, options|
34
+ expect(metric_name).to eq("rails.request.time.view")
35
+ expect(duration).to be_a(Float).and be > 0
36
+ expect(options[:tags]).to match_array(%w(method:get status:200 action:index controller:posts format:html))
37
+ end
38
+
39
+ get posts_path
40
+ end
41
+ end
9
42
  end
10
43
  end
11
44
  end
@@ -4,7 +4,7 @@ require "sneakers"
4
4
  require "sneakers/runner"
5
5
  require "external_sneaker"
6
6
 
7
- describe TestWorker, "This is manual labor as we can't verify that sneakers is running", :skip do
7
+ describe TestWorker do
8
8
  include FileHelper
9
9
  let(:message) do
10
10
  {
@@ -12,12 +12,10 @@ describe TestWorker, "This is manual labor as we can't verify that sneakers is r
12
12
  body: "Hot",
13
13
  }
14
14
  end
15
- let(:logger) { Sapience[described_class] }
16
15
 
17
16
  before do
18
- @sneakers_worker = ExternalSneaker.new("rake sneakers:run")
17
+ @sneakers_worker = ExternalSneaker.new("rake sneakers:run", described_class)
19
18
  @sneakers_worker.start
20
-
21
19
  Sneakers.publish(
22
20
  message.to_json,
23
21
  to_queue: described_class::QUEUE_NAME,
@@ -30,7 +28,14 @@ describe TestWorker, "This is manual labor as we can't verify that sneakers is r
30
28
  delete_file(described_class::VERIFICATION_FILE)
31
29
  end
32
30
 
31
+ # TODO: Possible make this less flaky or run it with retry (rspec-retry)
33
32
  it "runs properly" do
34
- wait(30.seconds).for { File.exist?(described_class::VERIFICATION_FILE) }.to eq(true)
33
+ count = 0
34
+ until File.exist?(described_class::VERIFICATION_FILE)
35
+ sleep 0.1
36
+ count += 1
37
+ expect(true).to be(false) if count > 240
38
+ end
39
+ expect(true).to be(true)
35
40
  end
36
41
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sapience
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-09-27 00:00:00.000000000 Z
12
+ date: 2016-09-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: concurrent-ruby
@@ -289,14 +289,19 @@ files:
289
289
  - lib/sapience/extensions/action_cable/tagged_logger_proxy.rb
290
290
  - lib/sapience/extensions/action_controller/live.rb
291
291
  - lib/sapience/extensions/action_controller/log_subscriber.rb
292
+ - lib/sapience/extensions/action_controller/notifications.rb
292
293
  - lib/sapience/extensions/action_dispatch/debug_exceptions.rb
293
294
  - lib/sapience/extensions/action_view/log_subscriber.rb
294
295
  - lib/sapience/extensions/action_view/streaming_template_renderer.rb
295
296
  - lib/sapience/extensions/active_job/logging.rb
297
+ - lib/sapience/extensions/active_job/notifications.rb
296
298
  - lib/sapience/extensions/active_model_serializers/logging.rb
297
299
  - lib/sapience/extensions/active_record/log_subscriber.rb
300
+ - lib/sapience/extensions/active_record/notifications.rb
298
301
  - lib/sapience/extensions/grape/middleware/logging.rb
302
+ - lib/sapience/extensions/grape/notifications.rb
299
303
  - lib/sapience/extensions/grape/timings.rb
304
+ - lib/sapience/extensions/notifications.rb
300
305
  - lib/sapience/extensions/rails/rack/logger.rb
301
306
  - lib/sapience/extensions/rails/rack/logger_info_as_debug.rb
302
307
  - lib/sapience/formatters/base.rb
@@ -353,6 +358,7 @@ files:
353
358
  - test_apps/rails/app/helpers/application_helper.rb
354
359
  - test_apps/rails/app/helpers/posts_helper.rb
355
360
  - test_apps/rails/app/jobs/application_job.rb
361
+ - test_apps/rails/app/jobs/test_job.rb
356
362
  - test_apps/rails/app/mailers/application_mailer.rb
357
363
  - test_apps/rails/app/models/application_record.rb
358
364
  - test_apps/rails/app/models/concerns/.keep
@@ -404,6 +410,7 @@ files:
404
410
  - test_apps/rails/lib/assets/.keep
405
411
  - test_apps/rails/lib/external_sneaker.rb
406
412
  - test_apps/rails/lib/tasks/.keep
413
+ - test_apps/rails/lib/tasks/sneakers.rake
407
414
  - test_apps/rails/public/404.html
408
415
  - test_apps/rails/public/422.html
409
416
  - test_apps/rails/public/500.html
@@ -417,6 +424,7 @@ files:
417
424
  - test_apps/rails/spec/fixtures/sapience.yml
418
425
  - test_apps/rails/spec/helpers/posts_helper_spec.rb
419
426
  - test_apps/rails/spec/integration/sapience_spec.rb
427
+ - test_apps/rails/spec/jobs/test_job_spec.rb
420
428
  - test_apps/rails/spec/models/post_spec.rb
421
429
  - test_apps/rails/spec/models/user_spec.rb
422
430
  - test_apps/rails/spec/rails_helper.rb