StatsCollect 0.1.0.20101220
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/AUTHORS +2 -0
- data/ChangeLog +5 -0
- data/Credits +31 -0
- data/LICENSE +31 -0
- data/README +18 -0
- data/ReleaseInfo +8 -0
- data/StatsCollect.conf.rb.example +91 -0
- data/TODO +1 -0
- data/bin/StatsCollect.rb +30 -0
- data/lib/StatsCollect/Backends/MySQL.rb +190 -0
- data/lib/StatsCollect/Backends/Terminal.rb +157 -0
- data/lib/StatsCollect/Locations/AddThis.rb +65 -0
- data/lib/StatsCollect/Locations/Facebook.rb +62 -0
- data/lib/StatsCollect/Locations/FacebookArtist.rb +63 -0
- data/lib/StatsCollect/Locations/FacebookLike.rb +51 -0
- data/lib/StatsCollect/Locations/GoogleSearch.rb +39 -0
- data/lib/StatsCollect/Locations/MySpace.rb +224 -0
- data/lib/StatsCollect/Locations/ReverbNation.rb +143 -0
- data/lib/StatsCollect/Locations/Tweets.rb +51 -0
- data/lib/StatsCollect/Locations/Twitter.rb +46 -0
- data/lib/StatsCollect/Locations/Youtube.rb +69 -0
- data/lib/StatsCollect/Notifiers/None.rb +24 -0
- data/lib/StatsCollect/Notifiers/SendMail.rb +34 -0
- data/lib/StatsCollect/Stats.rb +447 -0
- data/lib/StatsCollect/StatsProxy.rb +106 -0
- metadata +89 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module StatsCollect
|
7
|
+
|
8
|
+
module Locations
|
9
|
+
|
10
|
+
class Twitter
|
11
|
+
|
12
|
+
# Execute the plugin.
|
13
|
+
# This method has to add the stats and errors to the proxy.
|
14
|
+
# It can filter only objects and categories given.
|
15
|
+
# It has access to its configuration.
|
16
|
+
#
|
17
|
+
# Parameters:
|
18
|
+
# * *oStatsProxy* (_StatsProxy_): The stats proxy to be used to populate stats
|
19
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The configuration associated to this plugin
|
20
|
+
# * *iLstObjects* (<em>list<String></em>): List of objects to filter (can be empty for all)
|
21
|
+
# * *iLstCategories* (<em>list<String></em>): List of categories to filter (can be empty for all)
|
22
|
+
def execute(oStatsProxy, iConf, iLstObjects, iLstCategories)
|
23
|
+
if ((oStatsProxy.isObjectIncluded?('Global')) and
|
24
|
+
((oStatsProxy.isCategoryIncluded?('Following')) or
|
25
|
+
(oStatsProxy.isCategoryIncluded?('Followers')) or
|
26
|
+
(oStatsProxy.isCategoryIncluded?('Lists followers')) or
|
27
|
+
(oStatsProxy.isCategoryIncluded?('Tweets'))))
|
28
|
+
require 'mechanize'
|
29
|
+
lMechanizeAgent = Mechanize.new
|
30
|
+
lProfilePage = lMechanizeAgent.get("http://twitter.com/#{iConf[:Name]}")
|
31
|
+
lNbrFollowing = Integer(lProfilePage.root.css('span#following_count').first.content.strip)
|
32
|
+
lNbrFollowers = Integer(lProfilePage.root.css('span#follower_count').first.content.strip)
|
33
|
+
lNbrLists = Integer(lProfilePage.root.css('span#lists_count').first.content.strip)
|
34
|
+
lNbrTweets = Integer(lProfilePage.root.css('span#update_count').first.content.strip)
|
35
|
+
oStatsProxy.addStat('Global', 'Following', lNbrFollowing)
|
36
|
+
oStatsProxy.addStat('Global', 'Followers', lNbrFollowers)
|
37
|
+
oStatsProxy.addStat('Global', 'Lists followers', lNbrLists)
|
38
|
+
oStatsProxy.addStat('Global', 'Tweets', lNbrTweets)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module StatsCollect
|
7
|
+
|
8
|
+
module Locations
|
9
|
+
|
10
|
+
class Youtube
|
11
|
+
|
12
|
+
# Execute the plugin.
|
13
|
+
# This method has to add the stats and errors to the proxy.
|
14
|
+
# It can filter only objects and categories given.
|
15
|
+
# It has access to its configuration.
|
16
|
+
#
|
17
|
+
# Parameters:
|
18
|
+
# * *oStatsProxy* (_StatsProxy_): The stats proxy to be used to populate stats
|
19
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The configuration associated to this plugin
|
20
|
+
# * *iLstObjects* (<em>list<String></em>): List of objects to filter (can be empty for all)
|
21
|
+
# * *iLstCategories* (<em>list<String></em>): List of categories to filter (can be empty for all)
|
22
|
+
def execute(oStatsProxy, iConf, iLstObjects, iLstCategories)
|
23
|
+
require 'mechanize'
|
24
|
+
lMechanizeAgent = Mechanize.new
|
25
|
+
lLoginForm = lMechanizeAgent.get('http://www.youtube.com').link_with(:text => 'Sign In').click.forms[1]
|
26
|
+
lLoginForm.Email = 'Muriel.Esteban@GMail.com'
|
27
|
+
lLoginForm.Passwd = 'M[K2LBVu0}xT|b<[<Q")'
|
28
|
+
lMechanizeAgent.submit(lLoginForm, lLoginForm.buttons.first).meta.first.click
|
29
|
+
if ((oStatsProxy.isCategoryIncluded?('Video plays')) or
|
30
|
+
(oStatsProxy.isCategoryIncluded?('Video likes')) or
|
31
|
+
(oStatsProxy.isCategoryIncluded?('Video dislikes')) or
|
32
|
+
(oStatsProxy.isCategoryIncluded?('Video comments')) or
|
33
|
+
(oStatsProxy.isCategoryIncluded?('Video responses')))
|
34
|
+
getVideos(oStatsProxy, lMechanizeAgent)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get the videos statistics
|
39
|
+
#
|
40
|
+
# Parameters:
|
41
|
+
# * *oStatsProxy* (_StatsProxy_): The stats proxy to be used to populate stats
|
42
|
+
# * *iMechanizeAgent* (_Mechanize_): The agent reading pages
|
43
|
+
def getVideos(oStatsProxy, iMechanizeAgent)
|
44
|
+
lVideosPage = iMechanizeAgent.get('http://www.youtube.com/my_videos')
|
45
|
+
# List of videos read (used for display)
|
46
|
+
lLstVideosRead = []
|
47
|
+
lVideosPage.root.css('li.vm-video-item').each do |iVideoNode|
|
48
|
+
lVideoTitle = iVideoNode.css('div.vm-video-title a').first.content
|
49
|
+
lMetricNodes = iVideoNode.css('div.vm-video-metrics dl dd')
|
50
|
+
lNbrPlays = Integer(lMetricNodes[0].css('a').first.content.strip)
|
51
|
+
lNbrComments = Integer(lMetricNodes[1].content.strip)
|
52
|
+
lNbrResponses = Integer(lMetricNodes[2].content.strip)
|
53
|
+
lNbrLikes = Integer(lMetricNodes[3].content[0..-2].strip)
|
54
|
+
lNbrDislikes = Integer(lMetricNodes[4].content.strip)
|
55
|
+
oStatsProxy.addStat(lVideoTitle, 'Video plays', lNbrPlays)
|
56
|
+
oStatsProxy.addStat(lVideoTitle, 'Video comments', lNbrComments)
|
57
|
+
oStatsProxy.addStat(lVideoTitle, 'Video responses', lNbrResponses)
|
58
|
+
oStatsProxy.addStat(lVideoTitle, 'Video likes', lNbrLikes)
|
59
|
+
oStatsProxy.addStat(lVideoTitle, 'Video dislikes', lNbrDislikes)
|
60
|
+
lLstVideosRead << lVideoTitle
|
61
|
+
end
|
62
|
+
logDebug "#{lLstVideosRead.size} videos read: #{lLstVideosRead.join(', ')}"
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module StatsCollect
|
7
|
+
|
8
|
+
module Notifiers
|
9
|
+
|
10
|
+
class None
|
11
|
+
|
12
|
+
# Send a given notification
|
13
|
+
#
|
14
|
+
# Parameters:
|
15
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The notifier config
|
16
|
+
# * *iMessage* (_String_): Message to send
|
17
|
+
def sendNotification(iConf, iMessage)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module StatsCollect
|
7
|
+
|
8
|
+
module Notifiers
|
9
|
+
|
10
|
+
class SendMail
|
11
|
+
|
12
|
+
# Send a given notification
|
13
|
+
#
|
14
|
+
# Parameters:
|
15
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The notifier config
|
16
|
+
# * *iMessage* (_String_): Message to send
|
17
|
+
def sendNotification(iConf, iMessage)
|
18
|
+
require 'mail'
|
19
|
+
Mail.defaults do
|
20
|
+
delivery_method(:smtp, iConf[:SMTP])
|
21
|
+
end
|
22
|
+
Mail.deliver do
|
23
|
+
from iConf[:From]
|
24
|
+
to iConf[:To]
|
25
|
+
subject "Report of stats collection - #{DateTime.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
26
|
+
body iMessage
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,447 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2009-2010 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'date'
|
7
|
+
require 'optparse'
|
8
|
+
require 'StatsCollect/StatsProxy'
|
9
|
+
|
10
|
+
module StatsCollect
|
11
|
+
|
12
|
+
# Stats orders statuses
|
13
|
+
# !!! Those constants are also defined in the RoR website.
|
14
|
+
STATS_ORDER_STATUS_TOBEPROCESSED = 0
|
15
|
+
STATS_ORDER_STATUS_RECOVERABLE_ERROR = 1
|
16
|
+
STATS_ORDER_STATUS_UNRECOVERABLE_ERROR = 2
|
17
|
+
# Value types
|
18
|
+
# !!! Those constants are also defined in the RoR website.
|
19
|
+
STATS_VALUE_TYPE_INTEGER = 0
|
20
|
+
STATS_VALUE_TYPE_FLOAT = 1
|
21
|
+
STATS_VALUE_TYPE_PERCENTAGE = 2
|
22
|
+
STATS_VALUE_TYPE_UNKNOWN = 3
|
23
|
+
|
24
|
+
class Stats
|
25
|
+
|
26
|
+
# Constructor
|
27
|
+
def initialize
|
28
|
+
# Parse for available Locations
|
29
|
+
require 'rUtilAnts/Plugins'
|
30
|
+
RUtilAnts::Plugins::initializePlugins
|
31
|
+
parsePluginsFromDir('Locations', "#{File.expand_path(File.dirname(__FILE__))}/Locations", 'StatsCollect::Locations')
|
32
|
+
parsePluginsFromDir('Backends', "#{File.expand_path(File.dirname(__FILE__))}/Backends", 'StatsCollect::Backends')
|
33
|
+
parsePluginsFromDir('Notifiers', "#{File.expand_path(File.dirname(__FILE__))}/Notifiers", 'StatsCollect::Notifiers')
|
34
|
+
|
35
|
+
@Backend = nil
|
36
|
+
@Notifier = nil
|
37
|
+
@ConfigFile = nil
|
38
|
+
@DisplayHelp = false
|
39
|
+
|
40
|
+
# The command line parser
|
41
|
+
@Options = OptionParser.new
|
42
|
+
@Options.banner = 'StatsCollect.rb [--help] [--debug] --backend <Backend> --notifier <Notifier> --config <ConfigFile>'
|
43
|
+
@Options.on( '--backend <Backend>', String,
|
44
|
+
"<Backend>: Backend to be used. Available backends are: #{getPluginNames('Backends').join(', ')}",
|
45
|
+
'Specify the backend to be used') do |iArg|
|
46
|
+
@Backend = iArg
|
47
|
+
end
|
48
|
+
@Options.on( '--notifier <Notifier>', String,
|
49
|
+
"<Notifier>: Notifier used to send notifications. Available notifiers are: #{getPluginNames('Notifiers').join(', ')}",
|
50
|
+
'Specify the notifier to be used') do |iArg|
|
51
|
+
@Notifier = iArg
|
52
|
+
end
|
53
|
+
@Options.on( '--config <ConfigFile>', String,
|
54
|
+
'<ConfigFile>: The configuration file',
|
55
|
+
'Specify the configuration file') do |iArg|
|
56
|
+
@ConfigFile = iArg
|
57
|
+
end
|
58
|
+
@Options.on( '--help',
|
59
|
+
'Display help') do
|
60
|
+
@DisplayHelp = true
|
61
|
+
end
|
62
|
+
@Options.on( '--debug',
|
63
|
+
'Activate debug logs') do
|
64
|
+
activateLogDebug(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check that the environment is correctly set.
|
70
|
+
# This has to be called prior to calling collect, and exit should be made if error code is not 0.
|
71
|
+
#
|
72
|
+
# Parameters:
|
73
|
+
# * *iParams* (<em>list<String></em>): The parameters, as given in the command line
|
74
|
+
# Return:
|
75
|
+
# * _Integer_: Error code (given to exit) (0 = no error)
|
76
|
+
def setup(iParams)
|
77
|
+
rErrorCode = 0
|
78
|
+
|
79
|
+
lRemainingArgs = nil
|
80
|
+
begin
|
81
|
+
lRemainingArgs = @Options.parse(iParams)
|
82
|
+
if (!lRemainingArgs.empty?)
|
83
|
+
logErr "Unknown arguments: #{lRemainingArgs.join(' ')}"
|
84
|
+
logErr @Options
|
85
|
+
rErrorCode = 1
|
86
|
+
end
|
87
|
+
rescue Exception
|
88
|
+
logErr "Exception: #{$!}.\n#{$!.backtrace.join("\n")}"
|
89
|
+
rErrorCode = 2
|
90
|
+
end
|
91
|
+
if (rErrorCode == 0)
|
92
|
+
if (@DisplayHelp)
|
93
|
+
logMsg @Options
|
94
|
+
rErrorCode = 3
|
95
|
+
elsif (@Backend == nil)
|
96
|
+
logErr 'You must specify a backend.'
|
97
|
+
logErr @Options
|
98
|
+
rErrorCode = 4
|
99
|
+
elsif (@Notifier == nil)
|
100
|
+
logErr 'You must specify a notifier.'
|
101
|
+
logErr @Options
|
102
|
+
rErrorCode = 5
|
103
|
+
elsif (@ConfigFile == nil)
|
104
|
+
logErr 'You must specify a config file.'
|
105
|
+
logErr @Options
|
106
|
+
rErrorCode = 6
|
107
|
+
else
|
108
|
+
@Conf = nil
|
109
|
+
# Read the configuration file
|
110
|
+
begin
|
111
|
+
File.open(@ConfigFile, 'r') do |iFile|
|
112
|
+
@Conf = eval(iFile.read)
|
113
|
+
end
|
114
|
+
rescue
|
115
|
+
logErr "Invalid configuration file: #{@ConfigFile}"
|
116
|
+
rErrorCode = 7
|
117
|
+
end
|
118
|
+
if (rErrorCode == 0)
|
119
|
+
# Get the corresponding notifier
|
120
|
+
if (@Conf[:Notifiers][@Notifier] == nil)
|
121
|
+
logErr "Notifier #{@Notifier} has no configuration set up in configuration file #{@ConfigFile}"
|
122
|
+
rErrorCode = 8
|
123
|
+
else
|
124
|
+
@NotifierInstance, lError = getPluginInstance('Notifiers', @Notifier)
|
125
|
+
if (@NotifierInstance == nil)
|
126
|
+
logErr "Unable to instantiate notifier #{@Notifier}: #{lError}"
|
127
|
+
rErrorCode = 9
|
128
|
+
else
|
129
|
+
# Will we notify the user of the script execution ?
|
130
|
+
@NotifyUser = true
|
131
|
+
# Get the corresponding backend
|
132
|
+
if (@Conf[:Backends][@Backend] == nil)
|
133
|
+
logErr "Backend #{@Backend} has no configuration set up in configuration file #{@ConfigFile}"
|
134
|
+
rErrorCode = 10
|
135
|
+
else
|
136
|
+
@BackendInstance, lError = getPluginInstance('Backends', @Backend)
|
137
|
+
if (@BackendInstance == nil)
|
138
|
+
logErr "Unable to instantiate backend #{@Backend}: #{lError}"
|
139
|
+
rErrorCode = 11
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
return rErrorCode
|
149
|
+
end
|
150
|
+
|
151
|
+
# Execute the stats collection
|
152
|
+
#
|
153
|
+
# Return:
|
154
|
+
# * _Integer_: Error code (given to exit)
|
155
|
+
def collect
|
156
|
+
rErrorCode = 0
|
157
|
+
|
158
|
+
# Prevent concurrent execution
|
159
|
+
require 'tmpdir'
|
160
|
+
lLockFile = "#{Dir.tmpdir}/StatsCollect.lock"
|
161
|
+
if (File.exists?(lLockFile))
|
162
|
+
logErr "Another instance of stats collection is already running. Delete file #{lLockFile} if it is not."
|
163
|
+
begin
|
164
|
+
lDetails = nil
|
165
|
+
File.open(lLockFile, 'r') do |iFile|
|
166
|
+
lDetails = eval(iFile.read)
|
167
|
+
end
|
168
|
+
logErr "Details of the running instance: #{lDetails.inspect}"
|
169
|
+
rErrorCode = 12
|
170
|
+
rescue Exception
|
171
|
+
logErr "Invalid lock file #{lLockFile}: #{$!}."
|
172
|
+
rErrorCode = 13
|
173
|
+
end
|
174
|
+
else
|
175
|
+
File.open(lLockFile, 'w') do |oFile|
|
176
|
+
oFile << "
|
177
|
+
{
|
178
|
+
:ExecutionTime => '#{DateTime.now.strftime('%Y-%m-%d %H:%M:%S')}',
|
179
|
+
:PID => '#{Process.pid}'
|
180
|
+
}
|
181
|
+
"
|
182
|
+
end
|
183
|
+
begin
|
184
|
+
# Collect statistics
|
185
|
+
logInfo "[#{DateTime.now.strftime('%Y-%m-%d %H:%M:%S')}] - Begin collecting stats..."
|
186
|
+
@BackendInstance.initSession(@Conf[:Backends][@Backend])
|
187
|
+
# Get the stats orders to process
|
188
|
+
lFoundOrder = false
|
189
|
+
lTimeStamp, lLstLocations, lLstObjects, lLstCategories, lStatus = @BackendInstance.getNextStatsOrder
|
190
|
+
while (lTimeStamp != nil)
|
191
|
+
lFoundOrder = true
|
192
|
+
logInfo "Dequeued stats order: Time: #{lTimeStamp}, Locations: #{lLstLocations.join('|')}, Objects: #{lLstObjects.join('|')}, Categories: #{lLstCategories.join('|')}, Status: #{lStatus}"
|
193
|
+
begin
|
194
|
+
lRecoverableOrders, lUnrecoverableOrders = processOrder(lLstObjects, lLstCategories, lLstLocations)
|
195
|
+
# Add recoverable orders back
|
196
|
+
lRecoverableOrders.each do |iOrderInfo|
|
197
|
+
iLstRecoverableObjects, iLstRecoverableCategories, iLstRecoverableLocations = iOrderInfo
|
198
|
+
logInfo "Enqueue recoverable order: Locations: #{iLstRecoverableLocations.join('|')}, Objects: #{iLstRecoverableObjects.join('|')}, Categories: #{iLstRecoverableCategories.join('|')}"
|
199
|
+
@BackendInstance.putNewStatsOrder(DateTime.now, iLstRecoverableLocations, iLstRecoverableObjects, iLstRecoverableCategories, STATS_ORDER_STATUS_RECOVERABLE_ERROR)
|
200
|
+
end
|
201
|
+
# Add unrecoverable orders back
|
202
|
+
lUnrecoverableOrders.each do |iOrderInfo|
|
203
|
+
iLstUnrecoverableObjects, iLstUnrecoverableCategories, iLstUnrecoverableLocations = iOrderInfo
|
204
|
+
logInfo "Enqueue unrecoverable order: Locations: #{iLstUnrecoverableLocations.join('|')}, Objects: #{iLstUnrecoverableObjects.join('|')}, Categories: #{iLstUnrecoverableCategories.join('|')}"
|
205
|
+
@BackendInstance.putNewStatsOrder(lTimeStamp, iLstUnrecoverableLocations, iLstUnrecoverableObjects, iLstUnrecoverableCategories, STATS_ORDER_STATUS_UNRECOVERABLE_ERROR)
|
206
|
+
end
|
207
|
+
@BackendInstance.commit
|
208
|
+
rescue Exception
|
209
|
+
@BackendInstance.rollback
|
210
|
+
logErr "Exception while processing order #{lTimeStamp}, Locations: #{lLstLocations.join('|')}, Objects: #{lLstObjects.join('|')}, Categories: #{lLstCategories.join('|')}, Status: #{lStatus}: #{$!}.\n#{$!.backtrace.join("\n")}\n"
|
211
|
+
rErrorCode = 14
|
212
|
+
end
|
213
|
+
lTimeStamp, lLstLocations, lLstObjects, lLstCategories, lStatus = @BackendInstance.getNextStatsOrder
|
214
|
+
end
|
215
|
+
if (!lFoundOrder)
|
216
|
+
@NotifyUser = false
|
217
|
+
end
|
218
|
+
logInfo "[#{DateTime.now.strftime('%Y-%m-%d %H:%M:%S')}] - Stats collection finished."
|
219
|
+
File.unlink(lLockFile)
|
220
|
+
rescue Exception
|
221
|
+
logErr "Exception thrown while collecting stats: #{$!}.\n#{$!.backtrace.join("\n")}"
|
222
|
+
rErrorCode = 15
|
223
|
+
@NotifyUser = true
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
return rErrorCode
|
228
|
+
end
|
229
|
+
|
230
|
+
# Send notifications of a file content if necessary
|
231
|
+
#
|
232
|
+
# Parameters:
|
233
|
+
# * *iFileName* (_String_): The file containing notifications to be sent
|
234
|
+
def notify(iFileName)
|
235
|
+
if (@NotifyUser)
|
236
|
+
lMessage = nil
|
237
|
+
begin
|
238
|
+
File.open(iFileName, 'r') do |iFile|
|
239
|
+
lMessage = iFile.read
|
240
|
+
end
|
241
|
+
rescue Exception
|
242
|
+
lMessage = "Error while reading log file #{iFileName}: #{$!}"
|
243
|
+
end
|
244
|
+
@NotifierInstance.sendNotification(@Conf[:Notifiers][@Notifier], lMessage)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
private
|
249
|
+
|
250
|
+
# Process an order
|
251
|
+
#
|
252
|
+
# Parameters:
|
253
|
+
# * *iObjectsList* (<em>list<String></em>): List of objects to filter (can be empty for all)
|
254
|
+
# * *iCategoriesList* (<em>list<String></em>): List of categories to filter (can be empty for all)
|
255
|
+
# * *iLocationsList* (<em>list<String></em>): List of locations to filter (can be empty for all)
|
256
|
+
# Return:
|
257
|
+
# * <em>list<[list<String>,list<String>,list<String>]></em>: The list of orders (objects, categories, locations) that could not be performed due to recoverable errors
|
258
|
+
# * <em>list<[list<String>,list<String>,list<String>]></em>: The list of orders (objects, categories, locations) that could not be performed due to unrecoverable errors
|
259
|
+
def processOrder(iObjectsList, iCategoriesList, iLocationsList)
|
260
|
+
rRecoverableOrders = []
|
261
|
+
rUnrecoverableOrders = []
|
262
|
+
|
263
|
+
# For each location, call the relevant plugin
|
264
|
+
lPlugins = []
|
265
|
+
if (iLocationsList.empty?)
|
266
|
+
lPlugins = getPluginNames('Locations')
|
267
|
+
else
|
268
|
+
lPlugins = iLocationsList
|
269
|
+
end
|
270
|
+
lErrorPlugins = []
|
271
|
+
lPlugins.each do |iPluginName|
|
272
|
+
lPluginConf = nil
|
273
|
+
if (@Conf[:Locations] != nil)
|
274
|
+
lPluginConf = @Conf[:Locations][iPluginName]
|
275
|
+
end
|
276
|
+
lPlugin, lError = getPluginInstance('Locations', iPluginName)
|
277
|
+
if (lError == nil)
|
278
|
+
# Ask the plugin to perform the order
|
279
|
+
lStatsProxy = StatsProxy.new(iObjectsList, iCategoriesList)
|
280
|
+
logInfo "===== Call Location plugin #{iPluginName} to perform order..."
|
281
|
+
begin
|
282
|
+
lPlugin.execute(lStatsProxy, lPluginConf, iObjectsList, iCategoriesList)
|
283
|
+
rescue Exception
|
284
|
+
logErr "Exception thrown during plugin #{iPluginName} execution: #{$!}.\n#{$!.backtrace.join("\n")}"
|
285
|
+
lErrorPlugins << iPluginName
|
286
|
+
lError = true
|
287
|
+
end
|
288
|
+
if (lError == nil)
|
289
|
+
# Write the stats into the database
|
290
|
+
writeStats(lStatsProxy.StatsToAdd, iPluginName, iObjectsList, iCategoriesList)
|
291
|
+
# If the plugin failed on recoverable errors, note them
|
292
|
+
lStatsProxy.RecoverableOrders.each do |iOrderInfo|
|
293
|
+
iLstObjects, iLstCategories = iOrderInfo
|
294
|
+
# Filter them: if we did not want them, do not add them back
|
295
|
+
lObjectsToAdd = intersectLists(iLstObjects, iObjectsList)
|
296
|
+
lCategoriesToAdd = intersectLists(iLstCategories, iCategoriesList)
|
297
|
+
if ((lObjectsToAdd != nil) and
|
298
|
+
(lCategoriesToAdd != nil))
|
299
|
+
rRecoverableOrders << [ lObjectsToAdd, lCategoriesToAdd, [iPluginName] ]
|
300
|
+
end
|
301
|
+
end
|
302
|
+
# If the plugin failed on unrecoverable errors, note them
|
303
|
+
lStatsProxy.UnrecoverableOrders.each do |iOrderInfo|
|
304
|
+
iLstObjects, iLstCategories = iOrderInfo
|
305
|
+
# Filter them: if we did not want them, do not add them back
|
306
|
+
lObjectsToAdd = intersectLists(iLstObjects, iObjectsList)
|
307
|
+
lCategoriesToAdd = intersectLists(iLstCategories, iCategoriesList)
|
308
|
+
if ((lObjectsToAdd != nil) and
|
309
|
+
(lCategoriesToAdd != nil))
|
310
|
+
rUnrecoverableOrders << [ lObjectsToAdd, lCategoriesToAdd, [iPluginName] ]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
logInfo ''
|
315
|
+
else
|
316
|
+
logErr "Error while instantiating Location plugin #{iPluginName}: #{$!}."
|
317
|
+
lErrorPlugins << iPluginName
|
318
|
+
end
|
319
|
+
end
|
320
|
+
if (!lErrorPlugins.empty?)
|
321
|
+
rUnrecoverableOrders << [ iObjectsList, iCategoriesList, lErrorPlugins ]
|
322
|
+
end
|
323
|
+
|
324
|
+
return rRecoverableOrders, rUnrecoverableOrders
|
325
|
+
end
|
326
|
+
|
327
|
+
# Find names common to 2 different lists.
|
328
|
+
# Both lists can be empty to indicate all possible names.
|
329
|
+
# This method returns the intersection of both lists.
|
330
|
+
#
|
331
|
+
# Parameters:
|
332
|
+
# * *iLst1* (<em>list<String></em>): The first list
|
333
|
+
# * *iLst2* (<em>list<String></em>): The second list
|
334
|
+
# Return:
|
335
|
+
# * <em>list<String></em>: The resulting list. Can be empty for all possible names, or nil for no name.
|
336
|
+
def intersectLists(iLst1, iLst2)
|
337
|
+
rLstIntersection = nil
|
338
|
+
|
339
|
+
if (iLst1.empty?)
|
340
|
+
if (iLst2.empty?)
|
341
|
+
# Add all
|
342
|
+
rLstIntersection = []
|
343
|
+
else
|
344
|
+
rLstIntersection = iLst2
|
345
|
+
end
|
346
|
+
else
|
347
|
+
if (iLst2.empty?)
|
348
|
+
rLstIntersection = iLst1
|
349
|
+
else
|
350
|
+
# Filter
|
351
|
+
rLstIntersection = []
|
352
|
+
iLst1.each do |iObject|
|
353
|
+
if (iLst2.include?(iObject))
|
354
|
+
rLstIntersection << iObject
|
355
|
+
end
|
356
|
+
end
|
357
|
+
if (rLstIntersection.empty?)
|
358
|
+
rLstIntersection = nil
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
return rLstIntersection
|
364
|
+
end
|
365
|
+
|
366
|
+
# Write stats in the database.
|
367
|
+
# Apply some filter before.
|
368
|
+
#
|
369
|
+
# Parameters:
|
370
|
+
# * *iStatsToAdd* (<em>list<[TimeStamp,Object,Category,Value]></em>): The stats to write in the DB
|
371
|
+
# * *iLocation* (_String_): The location of these stats
|
372
|
+
# * *iLstObjects* (<em>list<String></em>): The filtering objects to write (can be empty for all)
|
373
|
+
# * *iLstCategories* (<em>list<String></em>): The filtering categories to write (can be empty for all)
|
374
|
+
def writeStats(iStatsToAdd, iLocation, iLstObjects, iLstCategories)
|
375
|
+
# Filter the stats we will really add
|
376
|
+
lStatsToBeCommitted = nil
|
377
|
+
if ((iLstObjects.empty?) and
|
378
|
+
(iLstCategories.empty?))
|
379
|
+
lStatsToBeCommitted = iStatsToAdd
|
380
|
+
else
|
381
|
+
# Filter
|
382
|
+
lStatsToBeCommitted = []
|
383
|
+
iStatsToAdd.each do |iStatsInfo|
|
384
|
+
iTimeStamp, iObject, iCategory, iValue = iStatsInfo
|
385
|
+
lOK = true
|
386
|
+
if (!iLstObjects.empty?)
|
387
|
+
lOK = iLstObjects.include?(iObject)
|
388
|
+
end
|
389
|
+
if ((lOK) and
|
390
|
+
(!iLstCategories.empty?))
|
391
|
+
lOK = iLstCategories.include?(iCategory)
|
392
|
+
end
|
393
|
+
if (lOK)
|
394
|
+
lStatsToBeCommitted << iStatsInfo
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
# Write stats if there are some
|
400
|
+
if (lStatsToBeCommitted.empty?)
|
401
|
+
logInfo 'No stats to be written after filtering.'
|
402
|
+
else
|
403
|
+
# Get the current locations from the DB to know if our location exists
|
404
|
+
lKnownLocations = @BackendInstance.getKnownLocations
|
405
|
+
lLocationID = lKnownLocations[iLocation]
|
406
|
+
if (lLocationID == nil)
|
407
|
+
# First create the new location and get its ID
|
408
|
+
logInfo "Creating new location: #{iLocation}"
|
409
|
+
lLocationID = @BackendInstance.addLocation(iLocation)
|
410
|
+
end
|
411
|
+
logDebug "Location used for those stats: #{iLocation} (#{lLocationID})"
|
412
|
+
# Get the list of categories, sorted by category name
|
413
|
+
lKnownCategories = @BackendInstance.getKnownCategories
|
414
|
+
# Get the list of objects, sorted by object name
|
415
|
+
# This map will eventually be completed if new objects are found among the stats to write.
|
416
|
+
lKnownObjects = @BackendInstance.getKnownObjects
|
417
|
+
# Add statistics
|
418
|
+
lStatsToBeCommitted.each do |iStatsInfo|
|
419
|
+
iTimeStamp, iObject, iCategory, iValue = iStatsInfo
|
420
|
+
# Check that the category exists
|
421
|
+
lValueType = nil
|
422
|
+
lCategoryID = nil
|
423
|
+
if (lKnownCategories[iCategory] == nil)
|
424
|
+
logWarn "Unknown stats category given by location #{iLocation}: #{iCategory}. It will be created with an Unknown value type."
|
425
|
+
lValueType = STATS_VALUE_TYPE_UNKNOWN
|
426
|
+
lCategoryID = @BackendInstance.addCategory(iCategory, lValueType)
|
427
|
+
lKnownCategories[iCategory] = [ lCategoryID, lValueType ]
|
428
|
+
else
|
429
|
+
lCategoryID, lValueType = lKnownCategories[iCategory]
|
430
|
+
end
|
431
|
+
# Check if we need to create the corresponding object
|
432
|
+
lObjectID = lKnownObjects[iObject]
|
433
|
+
if (lObjectID == nil)
|
434
|
+
logInfo "Creating new object: #{iObject}"
|
435
|
+
lObjectID = @BackendInstance.addObject(iObject)
|
436
|
+
lKnownObjects[iObject] = lObjectID
|
437
|
+
end
|
438
|
+
# Add the stat
|
439
|
+
@BackendInstance.addStat(iTimeStamp, lLocationID, lObjectID, lCategoryID, iValue, lValueType)
|
440
|
+
logDebug "Added stat: Time: #{iTimeStamp}, Location: #{iLocation} (#{lLocationID}), Object: #{iObject} (#{lObjectID}), Category: #{iCategory} (#{lCategoryID}), Value: #{iValue}"
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
end
|
446
|
+
|
447
|
+
end
|