appstats 0.7.0 → 0.8.0

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.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- appstats (0.7.0)
4
+ appstats (0.8.0)
5
5
  daemons
6
6
  net-scp
7
7
  rails (>= 2.3.0)
@@ -0,0 +1,13 @@
1
+ class CreateAppstatsHosts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :appstats_hosts do |t|
4
+ t.string :name
5
+ t.string :status
6
+ t.timestamps
7
+ end
8
+ end
9
+
10
+ def self.down
11
+ drop_table :appstats_hosts
12
+ end
13
+ end
data/db/schema.rb CHANGED
@@ -10,7 +10,7 @@
10
10
  #
11
11
  # It's strongly recommended to check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(:version => 20110210225606) do
13
+ ActiveRecord::Schema.define(:version => 20110215155830) do
14
14
 
15
15
  create_table "appstats_actions", :force => true do |t|
16
16
  t.string "name"
@@ -56,6 +56,13 @@ ActiveRecord::Schema.define(:version => 20110210225606) do
56
56
  add_index "appstats_entries", ["year", "month"], :name => "index_entries_by_month"
57
57
  add_index "appstats_entries", ["year"], :name => "index_entries_by_year"
58
58
 
59
+ create_table "appstats_hosts", :force => true do |t|
60
+ t.string "name"
61
+ t.string "status"
62
+ t.datetime "created_at"
63
+ t.datetime "updated_at"
64
+ end
65
+
59
66
  create_table "appstats_log_collectors", :force => true do |t|
60
67
  t.string "host"
61
68
  t.string "filename"
data/lib/appstats.rb CHANGED
@@ -12,6 +12,8 @@ require "#{File.dirname(__FILE__)}/appstats/logger"
12
12
  require "#{File.dirname(__FILE__)}/appstats/log_collector"
13
13
  require "#{File.dirname(__FILE__)}/appstats/query"
14
14
  require "#{File.dirname(__FILE__)}/appstats/result"
15
+ require "#{File.dirname(__FILE__)}/appstats/host"
16
+ require "#{File.dirname(__FILE__)}/appstats/parser"
15
17
  require "#{File.dirname(__FILE__)}/appstats/test_object"
16
18
 
17
19
  # required in the appstats.gemspec
@@ -0,0 +1,18 @@
1
+ module Appstats
2
+ class Host < ActiveRecord::Base
3
+ set_table_name "appstats_hosts"
4
+
5
+ attr_accessible :name, :status
6
+
7
+ def self.update_hosts
8
+ sql = "select distinct(host) from appstats_log_collectors where host not in (select name from appstats_hosts)"
9
+ count = 0
10
+ ActiveRecord::Base.connection.execute(sql).each do |row|
11
+ Appstats::Host.create(:name => row[0], :status => 'derived')
12
+ count += 1
13
+ end
14
+ count
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,239 @@
1
+
2
+ module Appstats
3
+ class Parser
4
+
5
+ attr_reader :raw_rules, :rules, :repeating, :raw_tokenize, :tokenize, :tokenize_no_spaces, :tokenize_regex, :tokenize_regex_no_spaces, :results, :raw_results, :constants
6
+
7
+ def initialize(data = {})
8
+ @raw_rules = data[:rules]
9
+ @raw_tokenize = data[:tokenize]
10
+ @repeating = data[:repeating] == true
11
+ @results = {}
12
+ @raw_results = []
13
+ update_tokens
14
+ update_rules
15
+ end
16
+
17
+ def parse(input)
18
+ @results = {}
19
+ @raw_results = []
20
+ return false if input.nil?
21
+ return false if @rules.size == 0
22
+
23
+ @rule_index = 0
24
+ @max_rule_index = @rules.size - 1
25
+ @previous_text_so_far = input.strip
26
+ @text_so_far = @previous_text_so_far
27
+ @remaining_constants = @constants.dup
28
+
29
+ while !@text_so_far.blank?
30
+ process_constant_if_present
31
+ break if @rule_index > @max_rule_index && !@repeating
32
+ @rule_index = 0 if @rule_index > @max_rule_index
33
+
34
+ rule = @rules[@rule_index]
35
+ @rule_index += 1
36
+
37
+ if rule.kind_of?(Hash)
38
+ if rule[:stop] == :constant
39
+ was_found = false
40
+ @remaining_constants.each_with_index do |k,index|
41
+ p = parse_word(@text_so_far,k,true)
42
+ if p[0].nil?
43
+ unset_rules_until(k)
44
+ else
45
+ (index-1).downto(0) do |i|
46
+ @remaining_constants.delete_at(i)
47
+ end
48
+ add_results(rule[:rule],p[0])
49
+ @text_so_far = p[1]
50
+ was_found = true
51
+ break
52
+ end
53
+ end
54
+ unless was_found
55
+ add_results(rule[:rule],@text_so_far)
56
+ @text_so_far = nil
57
+ end
58
+ else
59
+ p = parse_word(@text_so_far,rule[:stop],false)
60
+ add_results(rule[:rule],p[0])
61
+ @text_so_far = p[1]
62
+ end
63
+ end
64
+ break if @previous_text_so_far == @text_so_far
65
+ @previous_text_so_far = @text_so_far
66
+ end
67
+ remove_tokens_at_start(@text_so_far)
68
+ unset_rules_until(nil)
69
+ true
70
+ end
71
+
72
+ def self.parse_constant(current_text,constant)
73
+ answer = [nil,nil]
74
+ return answer if current_text.blank? || constant.nil?
75
+ current_text.strip!
76
+ m = current_text.match(/^(#{constant})(.*)$/im)
77
+ answer[0] = m[1] unless m.nil?
78
+ answer[1] = m.nil? ? current_text : m[2]
79
+ clean_parsed_word(answer)
80
+ end
81
+
82
+ def self.merge_regex_filter(a,b)
83
+ return "" if a.blank? && b.blank?
84
+ return "(#{a})" if b.blank?
85
+ return "(#{b})" if a.blank?
86
+ "(#{a}|#{b})"
87
+ end
88
+
89
+ def parse_word(current_text,stop_on,strict = false)
90
+ answer = [nil,nil]
91
+ return answer if current_text.blank? || stop_on.nil?
92
+ current_text.strip!
93
+
94
+ current_text = remove_tokens_at_start(current_text)
95
+
96
+ if stop_on == :end
97
+ filter = Parser.merge_regex_filter(nil,@tokenize_regex)
98
+ m = current_text.match(/^(.*?)(#{filter}.*)$/im)
99
+ if m.nil? || m[1].blank?
100
+ answer[0] = current_text
101
+ else
102
+ answer[0] = m[1]
103
+ answer[1] = m[2]
104
+ end
105
+ elsif stop_on == :space
106
+ filter = Parser.merge_regex_filter('\s',@tokenize_regex)
107
+ m = current_text.match(/^(.*?)(#{filter}.*)$/im)
108
+ if m.nil?
109
+ answer[0] = current_text
110
+ else
111
+ answer[0] = m[1]
112
+ answer[1] = m[2]
113
+ end
114
+ else
115
+ filter = Parser.merge_regex_filter(stop_on,@tokenize_regex)
116
+ m = current_text.match(/^(.*?)(#{filter}.*)$/im)
117
+ if strict
118
+ answer[0] = m[1] unless m.nil?
119
+ answer[1] = m.nil? ? current_text : m[2]
120
+ else
121
+ answer[0] = m.nil? ? current_text : m[1]
122
+ answer[1] = m[2] unless m.nil?
123
+ end
124
+ end
125
+ Parser.clean_parsed_word(answer)
126
+ end
127
+
128
+ private
129
+
130
+ def self.clean_parsed_word(answer)
131
+ answer[0].strip! unless answer[0].nil?
132
+ answer[1].strip! unless answer[1].nil?
133
+ answer[0] = nil if answer[0].blank?
134
+ answer[1] = nil if answer[1].blank?
135
+ answer
136
+ end
137
+
138
+ def process_constant_if_present
139
+ while process_tokens_if_present; end
140
+ to_delete = nil
141
+ @remaining_constants.each do |k|
142
+ p = Parser.parse_constant(@text_so_far,k)
143
+ next if p[0].nil?
144
+ to_delete = k
145
+ unset_rules_until(k)
146
+ add_constant(p[0])
147
+ @text_so_far = p[1]
148
+ end
149
+ @remaining_constants.delete(to_delete) unless to_delete.nil?
150
+ end
151
+
152
+ def process_tokens_if_present
153
+ found = false
154
+ @tokenize.each do |k|
155
+ p = Parser.parse_constant(@text_so_far,k)
156
+ next if p[0].nil?
157
+ add_constant(p[0])
158
+ @text_so_far = p[1]
159
+ found = true
160
+ end
161
+ found
162
+ end
163
+
164
+ def unset_rules_until(k)
165
+ @rules[@rule_index..-1].each do |rule|
166
+ @rule_index += 1
167
+ break if rule.eql?(k)
168
+ add_results(rule[:rule],nil) if rule.kind_of?(Hash)
169
+ end
170
+ end
171
+
172
+ def update_tokens
173
+ @tokenize = []
174
+ @tokenize_no_spaces = []
175
+ @tokenize_regex = nil
176
+ @tokenize_regex_no_spaces = nil
177
+ return if @raw_tokenize.blank?
178
+ @raw_tokenize.split(" ").each do |token|
179
+ current_token = token.upcase
180
+ current_token.gsub!("(",'\(')
181
+ current_token.gsub!(")",'\)')
182
+ current_token.gsub!("|",'\|')
183
+ @tokenize_no_spaces<< current_token
184
+ current_token = "\\s+#{current_token}" unless current_token.match(/.*[a-z].*/i).nil?
185
+ @tokenize<< current_token
186
+ end
187
+ @tokenize_regex_no_spaces = @tokenize_no_spaces.join("|")
188
+ @tokenize_regex = @tokenize.join("|")
189
+ end
190
+
191
+ def update_rules
192
+ @rules = []
193
+ @constants = []
194
+ current_rule = nil
195
+ return if @raw_rules.blank?
196
+ @raw_rules.split(" ").each do |rule|
197
+
198
+ if rule.starts_with?(":") && rule.size > 1
199
+ current_rule = { :rule => rule[1..-1].to_sym, :stop => :end }
200
+ previous_stop_on = :space
201
+ else
202
+ current_rule = rule.upcase
203
+ @constants<< current_rule
204
+ previous_stop_on = :constant
205
+ end
206
+
207
+ if @rules.last.kind_of?(Hash)
208
+ @rules.last[:stop] = previous_stop_on
209
+ end
210
+
211
+ @rules<< current_rule
212
+ end
213
+ end
214
+
215
+ def add_constant(value)
216
+ @raw_results<< value
217
+ end
218
+
219
+ def add_results(rule_name,value)
220
+ @raw_results<< { rule_name => value }
221
+ @results[rule_name] = value
222
+ end
223
+
224
+ def remove_tokens_at_start(current_text)
225
+ return current_text if current_text.blank?
226
+ current_text.blank?
227
+ loop do
228
+ break if @tokenize_regex.blank?
229
+ m = current_text.match(/^(#{@tokenize_regex_no_spaces})(.*)$/im)
230
+ break if m.nil? || m[1].blank?
231
+ add_constant(m[1])
232
+ current_text = m[2]
233
+ current_text.strip! unless current_text.nil?
234
+ end
235
+ current_text
236
+ end
237
+
238
+ end
239
+ end
@@ -2,8 +2,9 @@
2
2
  module Appstats
3
3
  class Query
4
4
 
5
+ @@nill_query = "select 0 from appstats_entries LIMIT 1"
5
6
  @@default = "1=1"
6
- attr_accessor :query, :action, :host, :date_range, :query_to_sql
7
+ attr_accessor :query, :action, :host, :date_range, :query_to_sql, :contexts
7
8
 
8
9
  def initialize(data = {})
9
10
  self.query=(data[:query])
@@ -28,17 +29,60 @@ module Appstats
28
29
  return @@default if m.nil?
29
30
  host = m[1]
30
31
  return @@default if host == '' or host.nil?
31
- "EXISTS (select * from appstats_log_collectors where appstats_entries.appstats_log_collector_id = id and host = '#{host}' )"
32
+ "EXISTS (select * from appstats_log_collectors where appstats_entries.appstats_log_collector_id = appstats_log_collectors.id and host = '#{host}' )"
32
33
  end
33
34
 
34
- def self.context_filter_to_sql(raw_input)
35
- return @@default if raw_input.nil?
36
- m = raw_input.match(/([^']+)=([^']+)/)
37
- return @@default if m.nil?
38
- key = m[1].strip
39
- value = m[2].strip
40
- return @@default if key == '' or key.nil?
41
- "EXISTS (select * from appstats_contexts where appstats_entries.id = appstats_contexts.appstats_entry_id and context_key='#{key}' and context_value='#{value}' )"
35
+ def self.contexts_filter_to_sql(raw_input)
36
+ context_parser = Appstats::Parser.new(:rules => ":context", :repeating => true, :tokenize => "|| && = <= >= <> != ( )")
37
+ return @@default if (raw_input.blank? || !context_parser.parse(raw_input))
38
+ sql = "EXISTS (select * from appstats_contexts where appstats_entries.id = appstats_contexts.appstats_entry_id and ("
39
+
40
+ status = :next
41
+ comparator = "="
42
+ context_parser.raw_results.each do |entry|
43
+ if entry.kind_of?(String)
44
+ sqlentry = sqlize(entry)
45
+ if Query.comparator?(entry) && status == :waiting_comparator
46
+ comparator = sqlize(entry)
47
+ status = :waiting_operand
48
+ else
49
+ sql += ")" if status == :waiting_comparator
50
+ sql += " #{sqlentry}"
51
+ status = :next
52
+ end
53
+ next
54
+ end
55
+ if status == :next
56
+ status = :waiting_comparator
57
+ sql += " (context_key='#{sqlclean(entry[:context])}'"
58
+ else
59
+ status = :next
60
+ sql += " and context_value#{comparator}'#{sqlclean(entry[:context])}')"
61
+ end
62
+ end
63
+ sql += ")" if status == :waiting_comparator
64
+ sql += "))"
65
+ sql
66
+ end
67
+
68
+ def self.sqlize(input)
69
+ return "and" if input == "&&"
70
+ return "or" if input == "||"
71
+ return "<>" if input == "!="
72
+ input
73
+ end
74
+
75
+ def self.sqlclean(raw_input)
76
+ return raw_input if raw_input.blank?
77
+ m = raw_input.match(/^['"](.*)['"]$/)
78
+ input = m.nil? ? raw_input : m[1]
79
+ input = input.gsub(/\\/, '\&\&').gsub(/'/, "''")
80
+ input
81
+ end
82
+
83
+ def self.comparator?(raw_input)
84
+ return false if raw_input.nil?
85
+ ["=","!=","<>",">","<",">=","<="].include?(raw_input)
42
86
  end
43
87
 
44
88
  private
@@ -49,35 +93,50 @@ module Appstats
49
93
  end
50
94
 
51
95
  def parse_query
52
- @query_to_sql = "select count(*) from appstats_entries"
53
- @action = nil
54
- @host = nil
55
- @date_range = DateRange.new
56
- return @query_to_sql if @query.nil?
57
- current_query = @query
58
-
59
- m = current_query.match(/^\s*(\#)\s*([^\s]*)\s*(.*)/)
60
- return @query_to_sql if m.nil?
61
- if m[1] == "#"
62
- @action = normalize_action_name(m[2])
63
- @query_to_sql += " where action = '#{@action}'"
64
- end
65
- current_query = m[3]
66
-
67
- m_on_server = current_query.match(/^(.*)?\s*on\s*server\s*(.*)$/)
68
- date_range_text = m_on_server.nil? ? current_query : m_on_server[1]
69
- if date_range_text.size > 0
70
- @date_range = DateRange.parse(date_range_text)
96
+ reset_query
97
+ return nil_query if @query.nil?
98
+ current_query = fix_legacy_structures(@query)
99
+
100
+ parser = Appstats::Parser.new(:rules => ":operation :action :date on :host where :contexts")
101
+ return nil_query unless parser.parse(current_query)
102
+
103
+ @operation = parser.results[:operation]
104
+ @action = normalize_action_name(parser.results[:action])
105
+ @date_range = DateRange.parse(parser.results[:date])
106
+ @host = parser.results[:host]
107
+ @contexts = parser.results[:contexts]
108
+
109
+ if @operation == "#"
110
+ @query_to_sql = "select count(*) from appstats_entries"
111
+ @query_to_sql += " where action = '#{@action}'" unless @action.blank?
71
112
  @query_to_sql += " and #{@date_range.to_sql}" unless @date_range.to_sql == "1=1"
113
+ @query_to_sql += " and #{Query.host_filter_to_sql(@host)}" unless @host.nil?
114
+ @query_to_sql += " and #{Query.contexts_filter_to_sql(@contexts)}" unless @contexts.nil?
72
115
  end
73
- return @query_to_sql if m_on_server.nil?
74
-
75
- @host = m_on_server[2]
76
- @query_to_sql += " and exists (select * from appstats_log_collectors where appstats_entries.appstats_log_collector_id = appstats_log_collectors.id and host = '#{@host}')"
77
116
 
78
117
  @query_to_sql
79
- end
80
-
118
+ end
119
+
120
+ def fix_legacy_structures(raw_input)
121
+ query = raw_input.gsub(/on\s*server/,"on")
122
+ query
123
+ end
124
+
125
+ def sql_for_conext(context_name,contact_value)
126
+ "EXISTS(select * from appstats_contexts where appstats_contexts.appstats_entry_id=appstats_entries.id and context_key='#{context_name}' and context_value='#{contact_value}' )"
127
+ end
128
+
129
+ def nil_query
130
+ @query_to_sql = @@nill_query
131
+ @query_to_sql
132
+ end
133
+
134
+ def reset_query
135
+ @action = nil
136
+ @host = nil
137
+ nil_query
138
+ @date_range = DateRange.new
139
+ end
81
140
 
82
141
  end
83
142
  end