sapience 1.0.3 → 1.0.4

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