focuslight 0.1.1

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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.env +13 -0
  3. data/.gitignore +21 -0
  4. data/.travis.yml +9 -0
  5. data/CHANGELOG.md +21 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/Procfile +3 -0
  9. data/Procfile-gem +3 -0
  10. data/README.md +162 -0
  11. data/Rakefile +37 -0
  12. data/bin/focuslight +7 -0
  13. data/config.ru +6 -0
  14. data/focuslight.gemspec +41 -0
  15. data/lib/focuslight.rb +6 -0
  16. data/lib/focuslight/cli.rb +56 -0
  17. data/lib/focuslight/config.rb +27 -0
  18. data/lib/focuslight/data.rb +258 -0
  19. data/lib/focuslight/graph.rb +240 -0
  20. data/lib/focuslight/init.rb +13 -0
  21. data/lib/focuslight/logger.rb +89 -0
  22. data/lib/focuslight/rrd.rb +393 -0
  23. data/lib/focuslight/validator.rb +220 -0
  24. data/lib/focuslight/version.rb +3 -0
  25. data/lib/focuslight/web.rb +614 -0
  26. data/lib/focuslight/worker.rb +97 -0
  27. data/public/css/bootstrap.min.css +7 -0
  28. data/public/favicon.ico +0 -0
  29. data/public/fonts/glyphicons-halflings-regular.eot +0 -0
  30. data/public/fonts/glyphicons-halflings-regular.svg +229 -0
  31. data/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  32. data/public/fonts/glyphicons-halflings-regular.woff +0 -0
  33. data/public/js/bootstrap.min.js +7 -0
  34. data/public/js/jquery-1.10.2.min.js +6 -0
  35. data/public/js/jquery-1.10.2.min.map +0 -0
  36. data/public/js/jquery.storageapi.min.js +2 -0
  37. data/public/js/site.js +214 -0
  38. data/spec/spec_helper.rb +3 -0
  39. data/spec/syntax_spec.rb +9 -0
  40. data/spec/validator_predefined_rules_spec.rb +177 -0
  41. data/spec/validator_result_spec.rb +27 -0
  42. data/spec/validator_rule_spec.rb +68 -0
  43. data/spec/validator_spec.rb +121 -0
  44. data/view/add_complex.erb +143 -0
  45. data/view/base.erb +200 -0
  46. data/view/docs.erb +125 -0
  47. data/view/edit.erb +102 -0
  48. data/view/edit_complex.erb +158 -0
  49. data/view/index.erb +19 -0
  50. data/view/list.erb +22 -0
  51. data/view/view.erb +42 -0
  52. data/view/view_graph.erb +16 -0
  53. metadata +345 -0
@@ -0,0 +1,27 @@
1
+ require "focuslight"
2
+
3
+ module Focuslight::Config
4
+ DEFAULT_DATADIR = File.expand_path('data', "#{__dir__}/../..")
5
+ DEFAULT_LOG_PATH = File.expand_path('log/application.log', "#{__dir__}/../..")
6
+
7
+ def self.get(name)
8
+ case name
9
+ when :datadir
10
+ ENV.fetch('DATADIR', DEFAULT_DATADIR)
11
+ when :float_support
12
+ ENV.fetch('FLOAT_SUPPORT', false)
13
+ when :dburl
14
+ ENV.fetch('DBURL', 'sqlite://data/gforecast.db')
15
+ when :log_path
16
+ ENV.fetch('LOG_PATH', DEFAULT_LOG_PATH)
17
+ when :log_level
18
+ ENV.fetch('LOG_LEVEL', 'info')
19
+ when :log_shift_age
20
+ ENV.fetch('LOG_SHIFT_AGE', '0')
21
+ when :log_shift_size
22
+ ENV.fetch('LOG_SHIFT_SIZE', '1048576')
23
+ else
24
+ raise ArgumentError, 'unknown configuration keyword'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,258 @@
1
+ require "focuslight"
2
+ require "focuslight/config"
3
+ require "focuslight/logger"
4
+ require "focuslight/graph"
5
+ require "sequel"
6
+
7
+ DB = Sequel.connect(Focuslight::Config.get(:dburl), logger: Focuslight.logger)
8
+
9
+ class Focuslight::Data
10
+ include Focuslight::Logger
11
+
12
+ def initialize
13
+ @datadir = Focuslight::Config.get(:datadir)
14
+ @floatings = Focuslight::Config.get(:float_support) == "y"
15
+
16
+ if DB.database_type == :sqlite
17
+ DB.run 'PRAGMA journal_mode = WAL'
18
+ DB.run 'PRAGMA synchronous = NORMAL'
19
+ end
20
+ @graphs = DB.from(:graphs)
21
+ @complex_graphs = DB.from(:complex_graphs)
22
+ end
23
+
24
+ def number_type
25
+ @floatings ? Float : Bignum
26
+ end
27
+
28
+ def create_tables
29
+ ntype = number_type
30
+ DB.transaction do
31
+
32
+ DB.create_table :graphs do
33
+ primary_key :id, Bignum # Notice that SQLite actually creates integer primary key
34
+ column :service_name, String, null: false
35
+ column :section_name, String, null: false
36
+ column :graph_name, String, null: false
37
+ column :number, ntype, default: 0
38
+ column :mode, String, default: "gauge", null: false
39
+ column :description, String, default: "", null: false
40
+ column :sort, Bignum, default: 0, null: false
41
+ column :color, String, default: "#00CC00", null: false
42
+ column :ulimit, ntype, default: 1000000000000000, null: false
43
+ column :llimit, ntype, default: 0, null: false
44
+ column :type, String, default: "AREA", null: false
45
+ String :meta, text: true
46
+ column :created_at, Bignum, null: false
47
+ column :updated_at, Bignum, null: false
48
+ unique [:service_name, :section_name, :graph_name]
49
+ end
50
+
51
+ DB.create_table :complex_graphs do
52
+ primary_key :id, Bignum # Notice that SQLite actually creates integer primary key
53
+ column :service_name, String, null: false
54
+ column :section_name, String, null: false
55
+ column :graph_name, String, null: false
56
+ column :number, ntype, default: 0
57
+ column :description, String, default: "", null: false
58
+ column :sort, Bignum, default: 0, null: false
59
+ String :meta, text: true
60
+ column :created_at, Bignum, null: false
61
+ column :updated_at, Bignum, null: false
62
+ unique [:service_name, :section_name, :graph_name]
63
+ end
64
+ end
65
+ end
66
+
67
+ def execute(*args)
68
+ DB.run(*args)
69
+ end
70
+
71
+ def transaction
72
+ DB.transaction do
73
+ yield DB
74
+ end
75
+ end
76
+
77
+ def get(service, section, graph)
78
+ data = @graphs.where(service_name: service, section_name: section, graph_name: graph).first
79
+ data && Focuslight::Graph.concrete(data)
80
+ end
81
+
82
+ def get_by_id(id)
83
+ data = @graphs[id: id]
84
+ data && Focuslight::Graph.concrete(data)
85
+ end
86
+
87
+ def get_by_id_for_rrdupdate(id, target=:normal) # get_by_id_for_rrdupdate_short == get_by_id_for_rrdupdate(id, :short)
88
+ data = @graphs[id: id]
89
+ return nil unless data
90
+ graph = Focuslight::Graph.concrete(data)
91
+ end
92
+
93
+ def update(service_name, section_name, graph_name, number, mode, color)
94
+ data = nil
95
+ DB.transaction do
96
+ data = @graphs.where(service_name: service_name, section_name: section_name, graph_name: graph_name).first
97
+ if data
98
+ graph = Focuslight::Graph.concrete(data)
99
+ if mode == 'count'
100
+ number += graph.number
101
+ end
102
+ if mode != 'modified' || (mode == 'modified' && graph.number != number)
103
+ color = graph.color if color.empty?
104
+ @graphs.where(id: graph.id).update(number: number, mode: mode, color: color, updated_at: Time.now.to_i)
105
+ end
106
+ else
107
+ color = '#' + ['33', '66', '99', 'cc'].shuffle.slice(0,3).join if color.empty?
108
+ # COLUMNS = %w(service_name section_name graph_name number mode color llimit created_at updated_at)
109
+ columns = Focuslight::SimpleGraph::COLUMNS.join(',')
110
+ # PLACEHOLDERS = COLUMNS.map{|c| '?'}
111
+ placeholders = Focuslight::SimpleGraph::PLACEHOLDERS.join(',')
112
+ current_time = Time.now.to_i
113
+ @graphs.insert(
114
+ service_name: service_name,
115
+ section_name: section_name,
116
+ graph_name: graph_name,
117
+ number: number,
118
+ mode: mode,
119
+ color: color,
120
+ llimit: -1000000000,
121
+ created_at: current_time,
122
+ updated_at: current_time)
123
+ end
124
+
125
+ data = @graphs.where(service_name: service_name, section_name: section_name, graph_name: graph_name).first
126
+ end # transaction
127
+
128
+ Focuslight::Graph.concrete(data)
129
+ end
130
+
131
+ def update_graph(id, args)
132
+ graph = get_by_id(id)
133
+ return nil unless graph
134
+
135
+ graph.update(args)
136
+ @graphs.where(id: graph.id)
137
+ .update(
138
+ service_name: graph.service,
139
+ section_name: graph.section,
140
+ graph_name: graph.graph,
141
+ description: graph.description,
142
+ sort: graph.sort,
143
+ color: graph.color,
144
+ type: graph.type,
145
+ llimit: graph.llimit,
146
+ ulimit: graph.ulimit,
147
+ meta: graph.meta
148
+ )
149
+ true
150
+ end
151
+
152
+ def update_graph_description(id, description)
153
+ @graphs.where(id: id).update(description: description)
154
+ true
155
+ end
156
+
157
+ def get_services
158
+ rows1 = @graphs.order(:service_name).all
159
+ rows2 = @complex_graphs.order(:service_name).all
160
+ (rows1 + rows2).map{|row| row[:service_name]}.uniq.sort
161
+ end
162
+
163
+ def get_sections(service)
164
+ rows1 = @graphs.select(:section_name).order(:section_name).where(service_name: service).all
165
+ rows2 = @complex_graphs.select(:section_name).order(:section_name).where(service_name: service).all
166
+ (rows1 + rows2).map{|row| row[:section_name]}.uniq.sort
167
+ end
168
+
169
+ def get_graphs(service, section)
170
+ rows1 = @graphs.order(Sequel.desc(:sort)).where(service_name: service, section_name: section).all
171
+ rows2 = @complex_graphs.order(Sequel.desc(:sort)).where(service_name: service, section_name: section).all
172
+ (rows1 + rows2).map{|row| Focuslight::Graph.concrete(row)}.sort{|a,b| b.sort <=> a.sort}
173
+ end
174
+
175
+ def get_all_graph_id
176
+ @graphs.select(:id).all
177
+ end
178
+
179
+ def get_all_graph_name
180
+ @graphs.select(:id, :service_name, :section_name, :graph_name).reverse_order(:service_name, :section_name, :graph_name).all
181
+ end
182
+
183
+ def get_all_graph_all
184
+ rows = @graphs.reverse_order(:service_name, :section_name, :graph_name).all
185
+ rows.map{|row| Focuslight::Graph.concrete(row)}
186
+ end
187
+
188
+ def remove(id)
189
+ DB.transaction do
190
+ @graphs.where(id: id).delete
191
+ end
192
+ end
193
+
194
+ def get_complex(service, section, graph)
195
+ data = @complex_graphs.where(service_name: service, section_name: section, graph_name: graph).first
196
+ data && Focuslight::Graph.concrete(data)
197
+ end
198
+
199
+ def get_complex_by_id(id)
200
+ data = @complex_graphs.where(id: id).first
201
+ data && Focuslight::Graph.concrete(data)
202
+ end
203
+
204
+ def create_complex(service, section, graph, args)
205
+ description = args[:description]
206
+ sort = args[:sort]
207
+ meta = Focuslight::ComplexGraph.meta_clean(args).to_json
208
+ now = Time.now.to_i
209
+ @complex_graphs.insert(
210
+ service_name: service,
211
+ section_name: section,
212
+ graph_name: graph,
213
+ description: description,
214
+ sort: sort.to_i,
215
+ meta: meta,
216
+ created_at: now,
217
+ updated_at: now,
218
+ )
219
+ get_complex(service, section, graph)
220
+ end
221
+
222
+ def update_complex(id, args)
223
+ graph = get_complex_by_id(id)
224
+ return nil unless graph
225
+
226
+ graph.update(args)
227
+ @complex_graphs.where(id: graph.id)
228
+ .update(
229
+ service_name: graph.service,
230
+ section_name: graph.section,
231
+ graph_name: graph.graph,
232
+ description: graph.description,
233
+ sort: graph.sort,
234
+ meta: graph.meta,
235
+ updated_at: Time.now.to_i,
236
+ )
237
+
238
+ get_complex_by_id(id)
239
+ end
240
+
241
+ def remove_complex(id)
242
+ @complex_graphs.where(id: id).delete
243
+ end
244
+
245
+ def get_all_complex_graph_id
246
+ @complex_graphs.select(:id).all
247
+ end
248
+
249
+ def get_all_complex_graph_name
250
+ @complex_graphs.select(:id, :service_name, :section_name, :graph_name).reverse_order(:service_name, :section_name, :graph_name).all
251
+ end
252
+
253
+ def get_all_complex_graph_all
254
+ rows = @complex_graphs.reverse_order(:service_name, :section_name, :graph_name)
255
+ return [] unless rows
256
+ rows.map{|row| Focuslight::Graph.concrete(row)}
257
+ end
258
+ end
@@ -0,0 +1,240 @@
1
+ require "focuslight"
2
+ require "focuslight/logger"
3
+
4
+ require "digest"
5
+ require "json"
6
+
7
+ module Focuslight
8
+ class Graph
9
+ include Focuslight::Logger
10
+
11
+ def self.concrete(row)
12
+ if row.has_key?(:mode) && row.has_key?(:type)
13
+ Focuslight::SimpleGraph.new(row)
14
+ else
15
+ Focuslight::ComplexGraph.new(row)
16
+ end
17
+ end
18
+
19
+ attr_accessor :id, :service, :section, :graph, :number, :description, :sort
20
+ attr_accessor :meta
21
+ attr_accessor :created_at_time, :updated_at_time
22
+
23
+ attr_accessor :c_type, :stack # for complex graph construction
24
+
25
+ def initialize(row)
26
+ @row_hash = row
27
+
28
+ @id = row[:id]
29
+ @service = row[:service_name]
30
+ @section = row[:section_name]
31
+ @graph = row[:graph_name]
32
+ @number = row[:number].to_i # NOT NULL DEFAULT 0
33
+ @description = row[:description] || ''
34
+ @sort = row[:sort].to_i # NOT NULL DEFAULT 0
35
+
36
+ @meta = row[:meta]
37
+ @parsed_meta = JSON.parse(@meta || '{}', :symbolize_names => true)
38
+
39
+ @created_at_time = Time.at(row[:created_at].to_i)
40
+ @updated_at_time = Time.at(row[:updated_at].to_i)
41
+ end
42
+
43
+ def path
44
+ [@service, @section, @graph]
45
+ end
46
+
47
+ def created_at
48
+ @created_at_time.strftime('%Y/%m/%d %T')
49
+ end
50
+
51
+ def updated_at
52
+ @updated_at_time.strftime('%Y/%m/%d %T')
53
+ end
54
+
55
+ def to_hash
56
+ {
57
+ id: @id, service_name: @service, section_name: @section, graph_name: @graph,
58
+ number: @number, description: @description, sort: @sort,
59
+ created_at: self.created_at(), updated_at: self.updated_at(),
60
+ }
61
+ end
62
+
63
+ def self.hash2request(hash)
64
+ hash = hash.dup
65
+ is_complex = hash.delete(:complex)
66
+
67
+ hash.delete(:id)
68
+ hash.delete(:created_at)
69
+ hash.delete(:updated_at)
70
+
71
+ return hash unless is_complex ##TODO concrete?
72
+
73
+ hash.delete(:number)
74
+ hash[:sumup] = (hash[:sumup] ? '1' : '0')
75
+
76
+ data_rows = hash.delete(:data)
77
+
78
+ first = data_rows.shift
79
+ hash['path-1'.to_sym] = first[:graph_id]
80
+ hash['type-1'.to_sym] = first[:type]
81
+
82
+ p2 = 'path-2'.to_sym
83
+ t2 = 'type-2'.to_sym
84
+ s2 = 'stack-2'.to_sym
85
+ hash[p2] = []
86
+ hash[t2] = []
87
+ hash[s2] = []
88
+ data_rows.each do |row|
89
+ hash[p2] << row[:graph_id]
90
+ hash[t2] << row[:type]
91
+ hash[s2] << (row[:stack] ? '1' : '0')
92
+ end
93
+
94
+ hash ##TODO concrete?
95
+ end
96
+ end
97
+
98
+ class SimpleGraph < Graph
99
+ COLUMNS = %w(service_name section_name graph_name number mode color llimit created_at updated_at)
100
+ PLACEHOLDERS = COLUMNS.map{|c| '?'}
101
+
102
+ attr_accessor :mode, :color, :ulimit, :llimit, :type
103
+
104
+ attr_reader :md5
105
+ attr_accessor :adjust, :adjustval, :unit
106
+
107
+ def initialize(row)
108
+ super
109
+
110
+ @mode = row[:mode] || 'gauge' # NOT NULL DEFAULT 'gauge'
111
+ @color = row[:color] || '#00CC00' # NOT NULL DEFAULT '#00CC00'
112
+ @ulimit = row[:ulimit] || 1000000000000000 # NOT NULL DEFAULT 1000000000000000
113
+ @llimit = row[:llimit] || 0
114
+ @type = row[:type] || 'AREA'
115
+
116
+ @md5 = Digest::MD5.hexdigest(@id.to_s)
117
+
118
+ @adjust = @parsed_meta.fetch(:adjust, '*')
119
+ @adjustval = @parsed_meta.fetch(:adjustval, '1')
120
+ @unit = @parsed_meta.fetch(:unit, '')
121
+ end
122
+
123
+ def to_hash
124
+ simple = {
125
+ mode: @mode, color: @color,
126
+ ulimit: @ulimit, llimit: @llimit,
127
+ type: @type,
128
+ adjust: @adjust, adjustval: @adjustval, unit: @unit,
129
+ complex: false,
130
+ md5: @md5, meta: @meta,
131
+ }
132
+ hash = super
133
+ hash.merge(simple)
134
+ end
135
+
136
+ def complex?
137
+ false
138
+ end
139
+
140
+ def update(args={})
141
+ meta = @parsed_meta.dup
142
+ args.each do |k, v|
143
+ case k.to_sym
144
+ when :number then @number = v
145
+ when :description then @description = v
146
+ when :sort then @sort = v
147
+ when :mode then @mode = v
148
+ when :color then @color = v
149
+ when :ulimit then @ulimit = v
150
+ when :llimit then @llimit = v
151
+ when :type then @type = v
152
+ else
153
+ meta[k.to_sym] = v
154
+ end
155
+ end
156
+ @parsed_meta = self.class.meta_clean(@parsed_meta.merge(meta))
157
+ @meta = @parsed_meta.to_json
158
+ end
159
+
160
+ def self.meta_clean(args={})
161
+ args.delete_if do |k,v|
162
+ %w(id service_name section_name graph_name number
163
+ description sort mode color ulimit llimit type).include?(k.to_s)
164
+ end
165
+ end
166
+ end
167
+
168
+ class ComplexGraph < Graph
169
+ attr_accessor :sumup, :data_rows
170
+ attr_reader :complex_graph
171
+
172
+ def initialize(row)
173
+ super
174
+
175
+ uri = [:'type-1', :'path-1'].map{|k| @parsed_meta[k]}.join(':') + ':0' # stack
176
+
177
+ data_rows = []
178
+
179
+ first_row = {
180
+ type: @parsed_meta[:'type-1'],
181
+ path: @parsed_meta[:'path-1'].to_i,
182
+ stack: false,
183
+ graph_id: @parsed_meta[:'path-1'].to_i,
184
+ }
185
+ data_rows << first_row
186
+
187
+ unless @parsed_meta[:'type-2'].is_a?(Array)
188
+ [:'type-2', :'path-2', :'stack-2'].each do |key|
189
+ @parsed_meta[key] = [@parsed_meta[key]].flatten
190
+ end
191
+ end
192
+
193
+ @parsed_meta[:'type-2'].each_with_index do |type, i|
194
+ t = @parsed_meta[:'type-2'][i]
195
+ p = @parsed_meta[:'path-2'][i].to_i
196
+ s = @parsed_meta[:'stack-2'][i].is_a?(String) ? !!(@parsed_meta[:'stack-2'][i] =~ /^(1|true)$/i) : !!@parsed_meta[:'stack-2'][i]
197
+ uri += ':' + [t, p, (s ? '1' : '0')].join(':')
198
+ data_rows << {type: t, path: p, stack: s, graph_id: p}
199
+ end
200
+
201
+ @sumup = @parsed_meta.fetch(:sumup, '0') != '0' # '0' is false
202
+ @data_rows = data_rows
203
+ @complex_graph = uri
204
+ end
205
+
206
+ def to_hash
207
+ complex = {
208
+ sumup: @sumup, data: @data_rows,
209
+ complex: true
210
+ }
211
+ hash = super
212
+ hash.merge(complex)
213
+ end
214
+
215
+ def complex?
216
+ true
217
+ end
218
+
219
+ def update(args={})
220
+ meta = @parsed_meta.dup
221
+ args.each do |k, v|
222
+ case k.to_sym
223
+ when :number then @number = v
224
+ when :description then @description = v
225
+ when :sort then @sort = v
226
+ else
227
+ meta[k.to_sym] = v
228
+ end
229
+ end
230
+ @parsed_meta = self.class.meta_clean(@parsed_meta.merge(meta))
231
+ @meta = @parsed_meta.to_json
232
+ end
233
+
234
+ def self.meta_clean(args={})
235
+ args.delete_if do |k,v|
236
+ %w(id service_name section_name graph_name number description sort).include?(k.to_s)
237
+ end
238
+ end
239
+ end
240
+ end