appstats 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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