optic-rails 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9768eeeb47c2c8c2eef8cdf95244e88644f2ee9455e8425ff20f184e9caab8ea
4
- data.tar.gz: edb291080a2cd6d69fca3cc7ce665dd7035a5a521610ccd93b7820fb5c1796c8
3
+ metadata.gz: 4862c212205f52f736e51c5b74da8d0bd141d941fe27adb6b872efa715a59aac
4
+ data.tar.gz: b77ac04f9ae6cccc80168fc5a77ef27c249c75a6767859611022e7aac941d1ef
5
5
  SHA512:
6
- metadata.gz: 24f8ed8c4713484642f57a814b9e3a4a2855e9094c10077a5d098f684a86109f0428af77cedb0ef76779b1a47b7dc07722d43846ffb5973c01d43a7648d3313a
7
- data.tar.gz: aff41fee8f75733a7498c5c112799589bac2e7fd9b6c9b661ef51f75a3fa1e729d4cfafa8773456569f301a88f19f8beb625b33c58bad25ce4dc0f5941c28a3c
6
+ metadata.gz: 93a490e91f071a3c1089b4072a6ef273f291115d27c828d5e73593329e6ca924833a64991fca2ad0a6534bc52dcef2c357bca92bee4c53b0dbd9959967451a1a
7
+ data.tar.gz: 5298316a14f2c3668c7a1f2968ee7d0ca5df05a797b8ea75d32f165d54672de3e5441233c327171f5d86b627fb6152e1615f45b3fc7cdc56e5a217de4448e0c1
data/Rakefile CHANGED
@@ -1,26 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
- require 'bundler/setup'
4
+ require "bundler/setup"
3
5
  rescue LoadError
4
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ puts "You must `gem install bundler` and `bundle install` to run rake tasks"
5
7
  end
6
8
 
7
- require 'rdoc/task'
9
+ require "rdoc/task"
8
10
 
9
11
  RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'Optic::Rails'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.md')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
12
+ rdoc.rdoc_dir = "rdoc"
13
+ rdoc.title = "Optic::Rails"
14
+ rdoc.options << "--line-numbers"
15
+ rdoc.rdoc_files.include("README.md")
16
+ rdoc.rdoc_files.include("lib/**/*.rb")
15
17
  end
16
18
 
17
- require 'bundler/gem_tasks'
19
+ require "bundler/gem_tasks"
18
20
 
19
- require 'rake/testtask'
21
+ require "rake/testtask"
20
22
 
21
23
  Rake::TestTask.new(:test) do |t|
22
- t.libs << 'test'
23
- t.pattern = 'test/**/*_test.rb'
24
+ t.libs << "test"
25
+ t.pattern = "test/**/*_test.rb"
24
26
  t.verbose = false
25
27
  end
26
28
 
data/lib/optic/rails.rb CHANGED
@@ -1,150 +1,65 @@
1
- require "optic/rails/railtie"
1
+ # frozen_string_literal: true
2
2
 
3
- require "rgl/adjacency"
4
- require "rgl/dot"
5
- require "rgl/dijkstra"
3
+ require "optic/rails/railtie"
6
4
 
7
5
  module Optic
8
6
  module Rails
9
- # From https://gist.github.com/hongo35/7513104
10
- class PageRank
11
- EPS = 0.00001
12
-
13
- def initialize(matrix)
14
- @dim = matrix.size
15
-
16
- @p = []
17
- @dim.times do |i|
18
- @p[i] = []
19
- @dim.times do |j|
20
- total = matrix[i].inject(:+)
21
- @p[i][j] = total == 0 ? 0 : matrix[i][j] / (total * 1.0)
22
- end
23
- end
24
- end
25
-
26
- def calc(curr, alpha)
27
- loop do
28
- prev = curr.clone
29
-
30
- @dim.times do |i|
31
- ip = 0
32
- @dim.times do |j|
33
- ip += @p.transpose[i][j] * prev[j]
7
+ class << self
8
+ def entities
9
+ with_connection do
10
+ {
11
+ schema_version: ActiveRecord::Migrator.current_version,
12
+ entities: active_record_klasses.map do |klass|
13
+ {
14
+ name: klass.name,
15
+ table_name: klass.table_name,
16
+ attribute_names: klass.attribute_names,
17
+ table_exists: klass.table_exists?,
18
+ associations: klass.reflect_on_all_associations.map do |reflection|
19
+ {
20
+ name: reflection.name,
21
+ macro: reflection.macro,
22
+ options: reflection.options.map { |k, v| [k, v.to_s] }.to_h,
23
+ klass_name: reflection.options[:polymorphic] ? nil : reflection.klass.name,
24
+ }
25
+ end
26
+ }
34
27
  end
35
- curr[i] = (alpha * ip) + ((1.0 - alpha) / @dim * 1.0)
36
- end
37
-
38
- err = 0
39
- @dim.times do |i|
40
- err += (prev[i] - curr[i]).abs
41
- end
42
-
43
- if err < EPS
44
- return curr
45
- elsif err.nan?
46
- raise "PageRank failed" # TODO just ignore and move on
47
- end
48
- end
49
- end
50
- end
51
-
52
- def self.qualified_primary_key(vertex)
53
- %Q|"#{vertex.table_name}"."#{vertex.primary_key}"|
54
- end
55
-
56
- def self.entity_graph
57
- graph = RGL::DirectedAdjacencyGraph.new
58
-
59
- base_klass = ApplicationRecord rescue ActiveRecord::Base
60
- klasses = ObjectSpace.each_object(Class).find_all { |klass| klass < base_klass }.find_all(&:table_exists?)
61
-
62
- graph.add_vertices *klasses
63
-
64
- klasses.each do |klass|
65
- klass.reflect_on_all_associations(:belongs_to).each do |reflection|
66
- next if reflection.options[:polymorphic] # TODO
67
-
68
- # TODO should the source be reflection.active_record or klass?
69
- graph.add_edge klass, reflection.klass
28
+ }
70
29
  end
71
30
  end
72
31
 
73
- graph
74
- end
75
-
76
- def self.get_entities
77
- graph = entity_graph
78
-
79
- # TODO send entire graph to server
80
- entities = graph.vertices.map { |v| { name: v.name, table_name: v.table_name } }
81
-
82
- {
83
- schema_version: ActiveRecord::Migrator.current_version,
84
- entities: entities # TODO also return entity attributes?
85
- }
86
- end
32
+ def metrics
33
+ with_connection do |connection|
34
+ result = { entity_totals: [] }
35
+ active_record_klasses.find_all(&:table_exists?).each do |klass|
36
+ count_query = klass.unscoped.select("COUNT(*)").to_sql
37
+ result[:entity_totals] << { name: klass.name, total: connection.execute(count_query).first["count"] }
38
+ end
87
39
 
88
- # Try to be defensive with our DB connection:
89
- # 1) Check a connection out from the thread pool instead of using an implicit one
90
- # 2) Make the connection read-only
91
- # 3) Time out any queries that take more than 100ms
92
- def self.with_connection
93
- ActiveRecord::Base.connection_pool.with_connection do |connection|
94
- connection.transaction do
95
- connection.execute "SET TRANSACTION READ ONLY"
96
- connection.execute "SET LOCAL statement_timeout = 100"
97
- yield connection
40
+ result
98
41
  end
99
42
  end
100
- end
101
-
102
- def self.get_metrics(pivot_name)
103
- with_connection do |connection|
104
- result = {entity_totals: []}
105
- pivot = nil
106
43
 
107
- if pivot_name
108
- pivot = pivot_name.constantize
109
- result[:pivot_name] = pivot.name
110
- result[:pivot_values] = connection.execute(pivot.unscoped.select("*").to_sql).to_a
111
- result[:pivoted_totals] = []
112
- end
113
-
114
- graph = entity_graph
115
-
116
- # Spit out counts for each entity by the customer pivot
117
-
118
- edge_weights = lambda { |_| 1 }
119
-
120
- graph.vertices.each do |vertex|
121
- count_query = vertex.unscoped.select("COUNT(*)").to_sql
122
- result[:entity_totals] << { name: vertex.name, total: connection.execute(count_query).first["count"] }
123
-
124
- if pivot && vertex != pivot
125
- # TODO weight edges to give preference to non-optional belongs_to (and other attributes?)
126
- path = graph.dijkstra_shortest_path(edge_weights, vertex, pivot)
127
- if path
128
- # Generate a SQL query to count the number of vertex instances grouped by pivot id, with appropriate joins from the path
129
- belongs_to_names = path.each_cons(2).map do |join_from, join_to|
130
- # TODO we shouldn't have to look up the edge again - use a graph model that allows us to annotate the edges with the reflections
131
- reflections = join_from.reflect_on_all_associations(:belongs_to).find_all { |reflection| !reflection.options[:polymorphic] && reflection.klass == join_to }
132
- # TODO warn if more than one reflection
133
- reflection = reflections.min_by { |r| r.options.size }
134
- reflection.name
135
- end
136
-
137
- joins = belongs_to_names.reverse.inject { |acc, elt| { elt => acc } }
138
- query = vertex.unscoped.joins(joins).group(qualified_primary_key(pivot)).select(qualified_primary_key(pivot), "COUNT(*)").to_sql
139
-
140
- result[:pivoted_totals] << { entity_name: vertex.name, totals: Hash[connection.execute(query).map { |record| [record["id"], record["count"]] }] }
141
- else
142
- # TODO print warning that we couldn't find a path from the pivot to the vertex
143
- end
44
+ private
45
+
46
+ # Try to be defensive with our DB connection:
47
+ # 1) Check a connection out from the thread pool instead of using an implicit one
48
+ # 2) Make the connection read-only
49
+ # 3) Time out any queries that take more than 100ms
50
+ def with_connection
51
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
52
+ connection.transaction do
53
+ connection.execute "SET TRANSACTION READ ONLY"
54
+ connection.execute "SET LOCAL statement_timeout = 100"
55
+ yield connection
144
56
  end
145
57
  end
58
+ end
146
59
 
147
- result
60
+ def active_record_klasses
61
+ base_klass = ApplicationRecord rescue ActiveRecord::Base
62
+ ObjectSpace.each_object(Class).find_all { |klass| klass < base_klass }
148
63
  end
149
64
  end
150
65
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_cable_client"
2
4
  require "eventmachine"
3
5
  require "logger"
@@ -31,7 +33,7 @@ module Optic
31
33
  logger.debug "Starting worker thread"
32
34
  worker = Thread.new do
33
35
  EventMachine.run do
34
- client = ActionCableClient.new(uri, { channel: "MetricsChannel" }, true, { "Authorization" => "Bearer #{project_key}" })
36
+ client = ActionCableClient.new(uri, { channel: "MetricsChannel" }, true, "Authorization" => "Bearer #{project_key}")
35
37
 
36
38
  client.connected do
37
39
  logger.debug "Optic agent connected"
@@ -64,11 +66,11 @@ module Optic
64
66
  case command
65
67
  when "request_schema"
66
68
  logger.debug "Optic agent got schema request"
67
- client.perform "schema", message: Optic::Rails.get_entities
69
+ client.perform "schema", message: Optic::Rails.entities
68
70
  logger.debug "Optic agent sent schema"
69
71
  when "request_metrics"
70
72
  logger.debug "Optic agent got metrics request"
71
- client.perform "metrics", message: Optic::Rails.get_metrics(message["message"]["pivot"])
73
+ client.perform "metrics", message: Optic::Rails.metrics
72
74
  logger.debug "Optic agent sent metrics"
73
75
  else
74
76
  logger.warn "Optic agent got unknown command: #{command}"
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Optic
2
4
  module Rails
3
- VERSION = '1.0.0'
5
+ VERSION = "1.0.1"
4
6
  end
5
7
  end
metadata CHANGED
@@ -1,20 +1,23 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optic-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Vaynshtok
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-31 00:00:00.000000000 Z
11
+ date: 2018-06-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: action_cable_client
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ - - ">="
18
21
  - !ruby/object:Gem::Version
19
22
  version: 2.0.2
20
23
  type: :runtime
@@ -22,6 +25,9 @@ dependencies:
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '2.0'
30
+ - - ">="
25
31
  - !ruby/object:Gem::Version
26
32
  version: 2.0.2
27
33
  - !ruby/object:Gem::Dependency
@@ -52,35 +58,9 @@ dependencies:
52
58
  - - "~>"
53
59
  - !ruby/object:Gem::Version
54
60
  version: '5.0'
55
- - !ruby/object:Gem::Dependency
56
- name: rgl
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 0.5.3
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 0.5.3
69
- - !ruby/object:Gem::Dependency
70
- name: sqlite3
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- description: Rails plugin for Optic.
61
+ description: optic.watch is the easiest way to get notified when business metrics
62
+ change. This gem intelligently collects metrics from your production database using
63
+ ActiveRecord to automatically understand your data.
84
64
  email:
85
65
  - avaynshtok@gmail.com
86
66
  executables: []
@@ -93,7 +73,6 @@ files:
93
73
  - lib/optic/rails.rb
94
74
  - lib/optic/rails/railtie.rb
95
75
  - lib/optic/rails/version.rb
96
- - lib/tasks/optic/rails_tasks.rake
97
76
  homepage: https://www.sutrolabs.com/
98
77
  licenses:
99
78
  - MIT
@@ -117,5 +96,5 @@ rubyforge_project:
117
96
  rubygems_version: 2.7.3
118
97
  signing_key:
119
98
  specification_version: 4
120
- summary: Rails plugin for Optic.
99
+ summary: optic.watch for Rails
121
100
  test_files: []
@@ -1,4 +0,0 @@
1
- # desc "Explaining what the task does"
2
- # task :optic_rails do
3
- # # Task goes here
4
- # end