optic-rails 0.0.1 → 0.0.2

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: ab25e10c6d253b5a2e50f6a91a022d100d5fc1a83809f75fa9f3e37535606423
4
- data.tar.gz: 5d25957749ec7f0278e5746066343575328a2f2d44fe91950e3885caee7ebac9
3
+ metadata.gz: 436b23a12e9fffc72997a8908f2d5b1344480d4320948407366a6139270e2de8
4
+ data.tar.gz: a762c9f379234a5a8d3f7dede9c5127a02803440332a91e3dace73682242aad8
5
5
  SHA512:
6
- metadata.gz: 7d86414206d90cea96912e44b6a1ec4a9c786a22667580fb807c565689574ee7c0952e2966998b33e2f7ec45bbe554bd68916b96e490c9735320bb087317aeda
7
- data.tar.gz: 5feb012e27ffbf48e38ee8adf74157cc15a0df68852351f304fa2371cb03b0a36bfe2530187f7a460e5d25a3d16ee1a315e4cb17fd9ae4cf12dd0578deb89c4b
6
+ metadata.gz: 5309ef3c52760546e558ce70466e39317524220f8cbbbc0e6213f7160fe95079b0313ffc0a50c1028b18db8c83f75b70adb1221dd69a38c8d7add9bb5bf8b06d
7
+ data.tar.gz: 710b2f963094f48b474b9df379437d4e96479c6a5dd0794a92e160a70484acd7a0341a0d74f4fc07fde1bac90b169a4fd6baae9e2c2fc33edc8dcf781f698f26
data/lib/optic/rails.rb CHANGED
@@ -1,7 +1,152 @@
1
1
  require "optic/rails/railtie"
2
2
 
3
+ require "rgl/adjacency"
4
+ require "rgl/dot"
5
+ require "rgl/dijkstra"
6
+
3
7
  module Optic
4
8
  module Rails
5
- # Your code goes here...
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]
34
+ 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 }
61
+
62
+ graph.add_vertices *klasses
63
+
64
+ klasses.each do |klass|
65
+ klass.reflect_on_all_associations(:belongs_to).each do |reflection|
66
+ # p klass, reflection
67
+ next if reflection.options[:polymorphic] # TODO
68
+
69
+ # TODO should the source be reflection.active_record or klass?
70
+ graph.add_edge klass, reflection.klass
71
+ end
72
+ end
73
+
74
+ graph
75
+ end
76
+
77
+ def self.get_entities
78
+ graph = entity_graph
79
+
80
+ # Run PageRank on the graph to order the vertices by interestingness
81
+ alpha = 0.5 # arbitrary! seems to work!
82
+ vertices = graph.vertices.sort_by(&:name)
83
+
84
+ adjacency_matrix = Array.new(vertices.size) do |i|
85
+ Array.new(vertices.size) do |j|
86
+ 0
87
+ end
88
+ end
89
+
90
+ graph.edges.each do |edge|
91
+ adjacency_matrix[vertices.index(edge.source)][vertices.index(edge.target)] = 1
92
+ end
93
+
94
+ # TODO run this calculation on the server instead, and pass the full graph (not just the node list)
95
+ page_rank = PageRank.new(adjacency_matrix)
96
+ init = Array.new(vertices.size, 1.0 / vertices.size.to_f)
97
+ ranks = page_rank.calc(init, alpha)
98
+ ranked_entities = vertices.zip(ranks).map { |v, r| { name: v.name, page_rank: r } }.sort_by { |record| record[:page_rank] }.reverse
99
+
100
+ {
101
+ schema_version: ActiveRecord::Migrator.current_version,
102
+ entities: ranked_entities # TODO also return entity attributes?
103
+ }
104
+ end
105
+
106
+ def self.get_metrics(pivot_name)
107
+ pivot = pivot_name.constantize
108
+
109
+ result = {
110
+ entity_totals: [],
111
+ pivot_name: pivot.name,
112
+ pivot_values: pivot.all.as_json, # TODO this is slow and possibly brings in sensitive info
113
+ pivoted_totals: []
114
+ # TODO also return computed "spanning" tree of objects from the pivot's POV (using the dijkstra paths from below)
115
+ }
116
+
117
+ graph = entity_graph
118
+
119
+ # Spit out counts for each entity by the customer pivot
120
+
121
+ edge_weights = lambda { |_| 1 }
122
+
123
+ graph.vertices.each do |vertex|
124
+ result[:entity_totals] << { name: vertex.name, total: vertex.count }
125
+
126
+ next if vertex == pivot # Skip pivoted metrics if this is the pivot
127
+
128
+ # TODO weight edges to give preference to non-optional belongs_to (and other attributes?)
129
+ path = graph.dijkstra_shortest_path(edge_weights, vertex, pivot)
130
+ if path
131
+ # Generate a SQL query to count the number of vertex instances grouped by pivot id, with appropriate joins from the path
132
+ belongs_to_names = path.each_cons(2).map do |join_from, join_to|
133
+ # 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
134
+ reflections = join_from.reflect_on_all_associations(:belongs_to).find_all { |reflection| !reflection.options[:polymorphic] && reflection.klass == join_to }
135
+ raise "Multiple belongs_to unsupported" unless reflections.size == 1 # TODO
136
+ reflections.first.name
137
+ end
138
+
139
+ joins = belongs_to_names.reverse.inject { |acc, elt| { elt => acc } }
140
+ query = vertex.unscoped.joins(joins).group(qualified_primary_key(pivot))
141
+
142
+ result[:pivoted_totals] << { entity_name: vertex.name, totals: query.count }
143
+ else
144
+ p "WARNING: No path from #{vertex.name} to #{pivot.name}"
145
+ end
146
+ end
147
+
148
+ result
149
+ end
150
+
6
151
  end
7
152
  end
@@ -1,6 +1,40 @@
1
1
  module Optic
2
2
  module Rails
3
3
  class Railtie < ::Rails::Railtie
4
+ initializer "optic_rails.launch_client_thread" do |app|
5
+ api_key = app.config.optic_api_key # TODO fail gracefully if missing
6
+ uri = app.config.optic_uri
7
+ puts "Launching Optic client thread"
8
+ Thread.new do
9
+
10
+ EventMachine.run do
11
+ puts "connecting"
12
+ client = ActionCableClient.new(uri, { channel: "MetricsChannel" }, true, { "Authorization" => "Bearer #{api_key}" })
13
+ client.connected do
14
+ puts "successfully connected"
15
+ end
16
+
17
+ client.errored { |msg| puts "ERROR: #{msg}" }
18
+
19
+ # called whenever a message is received from the server
20
+ client.received do |message|
21
+ puts "MESSAGE: #{message}"
22
+ command = message["message"]["command"]
23
+
24
+ case command
25
+ when "request_schema"
26
+ puts "Schema requested!"
27
+ client.perform "schema", message: Optic::Rails.get_entities
28
+ when "request_metrics"
29
+ puts "Metrics requested!"
30
+ client.perform "metrics", message: Optic::Rails.get_metrics(message["message"]["pivot"])
31
+ else
32
+ puts "unknown command!"
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
4
38
  end
5
39
  end
6
40
  end
@@ -1,5 +1,5 @@
1
1
  module Optic
2
2
  module Rails
3
- VERSION = '0.0.1'
3
+ VERSION = '0.0.2'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optic-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
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-14 00:00:00.000000000 Z
11
+ date: 2018-05-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 5.2.0.rc2
27
+ - !ruby/object:Gem::Dependency
28
+ name: rgl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.5.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.5.3
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: sqlite3
29
43
  requirement: !ruby/object:Gem::Requirement