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 +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
|