optic-rails 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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