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 +4 -4
- data/lib/optic/rails.rb +146 -1
- data/lib/optic/rails/railtie.rb +34 -0
- data/lib/optic/rails/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 436b23a12e9fffc72997a8908f2d5b1344480d4320948407366a6139270e2de8
|
4
|
+
data.tar.gz: a762c9f379234a5a8d3f7dede9c5127a02803440332a91e3dace73682242aad8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
#
|
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
|
data/lib/optic/rails/railtie.rb
CHANGED
@@ -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
|
data/lib/optic/rails/version.rb
CHANGED
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.
|
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-
|
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
|