SheepDog 0.1.0.20110705
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +2 -0
- data/ChangeLog +5 -0
- data/Credits +13 -0
- data/LICENSE +31 -0
- data/README +18 -0
- data/ReleaseInfo +8 -0
- data/bin/sheepdog.rb +24 -0
- data/lib/sheepdog/Executor.rb +288 -0
- data/lib/sheepdog/Monitors/LogFile.rb +74 -0
- data/lib/sheepdog/Monitors/Process.rb +131 -0
- data/lib/sheepdog/Notifiers/SendMail.rb +49 -0
- data/lib/sheepdog/Notifiers/StdOut.rb +42 -0
- data/lib/sheepdog/Report.rb +73 -0
- data/sheepdog.conf.rb.example +98 -0
- metadata +78 -0
data/AUTHORS
ADDED
data/ChangeLog
ADDED
data/Credits
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
= Projects used by Sheep Dog
|
2
|
+
|
3
|
+
== Ruby
|
4
|
+
* Yukihiro « matz » Matsumoto (http://www.rubyist.net/~matz/)
|
5
|
+
* http://www.ruby-lang.org/
|
6
|
+
* Thanks a lot Matz for this truly wonderful language !
|
7
|
+
|
8
|
+
== rUtilAnts
|
9
|
+
* Muriel Salvan (http://murielsalvan.users.sourceforge.net)
|
10
|
+
* http://rutilants.sourceforge.net
|
11
|
+
* Used for plugins and logging handling.
|
12
|
+
|
13
|
+
= People that helped a lot in developing SheepDog
|
data/LICENSE
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
The license stated herein is a copy of the BSD License (modified on July 1999).
|
3
|
+
The AUTHOR mentionned below refers to the list of people involved in the
|
4
|
+
creation and modification of any file included in the delivered package.
|
5
|
+
This list is found in the file named AUTHORS.
|
6
|
+
The AUTHORS and LICENSE files have to be included in any release of software
|
7
|
+
embedding source code of this package, or using it as a derivative software.
|
8
|
+
|
9
|
+
Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
10
|
+
|
11
|
+
Redistribution and use in source and binary forms, with or without
|
12
|
+
modification, are permitted provided that the following conditions are met:
|
13
|
+
|
14
|
+
1. Redistributions of source code must retain the above copyright notice,
|
15
|
+
this list of conditions and the following disclaimer.
|
16
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
17
|
+
this list of conditions and the following disclaimer in the documentation
|
18
|
+
and/or other materials provided with the distribution.
|
19
|
+
3. The name of the author may not be used to endorse or promote products
|
20
|
+
derived from this software without specific prior written permission.
|
21
|
+
|
22
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
23
|
+
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
24
|
+
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
25
|
+
EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
26
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
27
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
28
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
29
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
30
|
+
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
31
|
+
OF SUCH DAMAGE.
|
data/README
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
-- This file is best viewed when processed by rdoc.
|
2
|
+
++
|
3
|
+
|
4
|
+
= Sheep Dog
|
5
|
+
|
6
|
+
Simple command line tool that monitors files and processes and sends notifications or take corrective actions when problems arise. Monitor log files for errors, processes CPU and memory consumption (can kill if exceeding), respawn dead processes.
|
7
|
+
|
8
|
+
== Where is the documentation ?
|
9
|
+
|
10
|
+
Check the website at http://sheepdogsys.sourceforge.net
|
11
|
+
|
12
|
+
== Who wrote it ?
|
13
|
+
|
14
|
+
Check the AUTHORS[link:files/AUTHORS.html] file.
|
15
|
+
|
16
|
+
== What is the license ?
|
17
|
+
|
18
|
+
You can find out in the LICENSE[link:files/LICENSE.html] file.
|
data/ReleaseInfo
ADDED
data/bin/sheepdog.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
#--
|
3
|
+
# Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
4
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
5
|
+
#++
|
6
|
+
|
7
|
+
require 'rUtilAnts/Logging'
|
8
|
+
RUtilAnts::Logging::initializeLogging('','')
|
9
|
+
require 'tmpdir'
|
10
|
+
lLogFile = "#{Dir.tmpdir}/SheepDog_#{Process.pid}.log"
|
11
|
+
setLogFile(lLogFile)
|
12
|
+
logInfo 'Starting SheepDog'
|
13
|
+
require 'sheepdog/Executor'
|
14
|
+
|
15
|
+
lConfFileName = ARGV[0]
|
16
|
+
if (lConfFileName == nil)
|
17
|
+
logErr "Usage: sheepdog.rb <ConfigFileName>"
|
18
|
+
elsif (File.exists?(lConfFileName))
|
19
|
+
SheepDog::Executor.new.execute(eval(File.read(lConfFileName)))
|
20
|
+
else
|
21
|
+
logErr "Missing file: #{lConfFileName}"
|
22
|
+
end
|
23
|
+
|
24
|
+
File.unlink(lLogFile)
|
@@ -0,0 +1,288 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'time'
|
7
|
+
require 'fileutils'
|
8
|
+
require 'sheepdog/Report'
|
9
|
+
|
10
|
+
module SheepDog
|
11
|
+
|
12
|
+
class Executor
|
13
|
+
|
14
|
+
# Constructor
|
15
|
+
def initialize
|
16
|
+
# Parse plugins
|
17
|
+
require 'rUtilAnts/Plugins'
|
18
|
+
RUtilAnts::Plugins::initializePlugins
|
19
|
+
parsePluginsFromDir('Notifiers', "#{File.expand_path(File.dirname(__FILE__))}/Notifiers", 'SheepDog::Notifiers')
|
20
|
+
parsePluginsFromDir('Monitors', "#{File.expand_path(File.dirname(__FILE__))}/Monitors", 'SheepDog::Monitors')
|
21
|
+
end
|
22
|
+
|
23
|
+
# Execute a given configuration
|
24
|
+
#
|
25
|
+
# Parameters:
|
26
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The sheep dog configuration
|
27
|
+
def execute(iConf)
|
28
|
+
# Get the local database, storing dates of last reports sent...
|
29
|
+
lDatabaseFileName = "#{iConf[:WorkingDir]}/Database"
|
30
|
+
lDatabase = nil
|
31
|
+
if (File.exists?(lDatabaseFileName))
|
32
|
+
lDatabase = Marshal.load(File.read(lDatabaseFileName))
|
33
|
+
else
|
34
|
+
lDatabase = {
|
35
|
+
# The time of last sent reports, per notifer and monitor name
|
36
|
+
# map< MonitorName, map< NotifierName, Time > >
|
37
|
+
:LastReportsSent => {}
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
# The list of monitor reports to be sent at the end of the process, per notifier, along with their respective configuration
|
42
|
+
# map< NotifierName, map< MonitorName, list < [ NotificationConf, list< ReportFileName > ] > > >
|
43
|
+
lGroupedMonitorReports = {}
|
44
|
+
# The map of monitor reports that are sent by our run
|
45
|
+
# map< ReportFileName >
|
46
|
+
lSentReports = {}
|
47
|
+
# The map of monitor reports that will be sent by later runs
|
48
|
+
# map< ReportFileName >
|
49
|
+
lDelayedReports = {}
|
50
|
+
# Loop through the objects to monitor
|
51
|
+
iConf[:Monitors].each do |iMonitorName, iMonitorInfo|
|
52
|
+
# Check that it is a known monitor, by accessing the plugin
|
53
|
+
lMonitorPluginInstance, lError = getPluginInstance('Monitors', iMonitorInfo[:Type])
|
54
|
+
if (lMonitorPluginInstance == nil)
|
55
|
+
# Unknown monitor
|
56
|
+
logErr "Unknown Monitor #{iMonitorInfo[:Type]}: #{lError}. Ignoring corresponding monitoring process. Please check configuration."
|
57
|
+
else
|
58
|
+
# Create the report to be filled by this process
|
59
|
+
lReport = Report.new
|
60
|
+
# Create the monitor configuration dir
|
61
|
+
lMonitorDir = "#{iConf[:WorkingDir]}/#{iMonitorName}"
|
62
|
+
FileUtils::mkdir_p(lMonitorDir)
|
63
|
+
# Set instance variables and methods for this monitor
|
64
|
+
lMonitorPluginInstance.instance_variable_set(:@SheepDogConf, iConf)
|
65
|
+
lMonitorPluginInstance.instance_variable_set(:@Report, lReport)
|
66
|
+
lMonitorPluginInstance.instance_variable_set(:@MonitorDir, lMonitorDir)
|
67
|
+
if (!lMonitorPluginInstance.respond_to?(:report))
|
68
|
+
# Report an entry
|
69
|
+
#
|
70
|
+
# Parameters:
|
71
|
+
# * *iEntry* (_String_): Entry to be reported
|
72
|
+
def lMonitorPluginInstance.report(iEntry)
|
73
|
+
@Report.addEntry(iEntry)
|
74
|
+
logInfo "Report: #{iEntry}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# Call this monitor
|
78
|
+
begin
|
79
|
+
logInfo "Executing monitoring process #{iMonitorName} ..."
|
80
|
+
lMonitorPluginInstance.execute(iMonitorInfo)
|
81
|
+
rescue Exception
|
82
|
+
logErr "Exception while executing monitor #{iMonitorName}: #{$!}.\n#{$!.backtrace.join("\n")}"
|
83
|
+
report "!!! Exception while executing monitor #{iMonitorName}: #{$!}.\n#{$!.backtrace.join("\n")}"
|
84
|
+
end
|
85
|
+
# If this report is not empty, save it in a file
|
86
|
+
lCurrentReportFileName = nil
|
87
|
+
lCurrentReportTime = nil
|
88
|
+
if (!lReport.empty?)
|
89
|
+
lCurrentReportTime = Time.now.utc
|
90
|
+
lCurrentReportFileName = "#{lMonitorDir}/Report_#{lCurrentReportTime.strftime('%Y-%m-%d-%H-%M-%S')}"
|
91
|
+
File.open(lCurrentReportFileName, 'w') do |oFile|
|
92
|
+
oFile.write(Marshal.dump(lReport))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
# Get the list of reports to send, per time
|
96
|
+
# map< Time, FileName >
|
97
|
+
lReportFiles = {}
|
98
|
+
Dir.glob("#{lMonitorDir}/Report_*").each do |iReportFile|
|
99
|
+
lMatch = File.basename(iReportFile).match(/^Report_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/)
|
100
|
+
if (lMatch == nil)
|
101
|
+
logErr "Invalid file report name: #{iReportFile}. Ignoring it."
|
102
|
+
else
|
103
|
+
lReportFiles[Time.parse("#{lMatch[1]}-#{lMatch[2]}-#{lMatch[3]} #{lMatch[4]}:#{lMatch[5]}:#{lMatch[6]} UTC")] = iReportFile
|
104
|
+
end
|
105
|
+
end
|
106
|
+
# For each report file, compute the list of notifiers that will send it
|
107
|
+
if (!lReportFiles.empty?)
|
108
|
+
# There are some report files to be (maybe) sent.
|
109
|
+
# Loop through the notifiers.
|
110
|
+
iMonitorInfo[:Notifiers].each do |iNotifierName, iNotifierConf|
|
111
|
+
lNotifierID = iNotifierConf[:Type]
|
112
|
+
if (iNotifierConf[:GroupReports] == nil)
|
113
|
+
# Send the report now if it exists
|
114
|
+
if (lCurrentReportFileName != nil)
|
115
|
+
# Send [iMonitorInfo, [lCurrentReportFileName]] to iNotifierName
|
116
|
+
if (iNotifierConf[:GroupWithOtherMonitors] == true)
|
117
|
+
if (lGroupedMonitorReports[lNotifierID] == nil)
|
118
|
+
lGroupedMonitorReports[lNotifierID] = {}
|
119
|
+
end
|
120
|
+
if (lGroupedMonitorReports[lNotifierID][iMonitorName] == nil)
|
121
|
+
lGroupedMonitorReports[lNotifierID][iMonitorName] = []
|
122
|
+
end
|
123
|
+
lGroupedMonitorReports[lNotifierID][iMonitorName] << [ iNotifierConf, [ lCurrentReportFileName ] ]
|
124
|
+
else
|
125
|
+
notify(iConf, {lNotifierID => {iMonitorName => [ [ iNotifierConf, [lCurrentReportFileName] ] ] }}, lDelayedReports)
|
126
|
+
end
|
127
|
+
lSentReports[lCurrentReportFileName] = nil
|
128
|
+
# Remember last report sent
|
129
|
+
if (lDatabase[:LastReportsSent][iMonitorName] == nil)
|
130
|
+
lDatabase[:LastReportsSent][iMonitorName] = {}
|
131
|
+
end
|
132
|
+
lDatabase[:LastReportsSent][iMonitorName][iNotifierName] = lCurrentReportTime
|
133
|
+
end
|
134
|
+
else
|
135
|
+
# Get the interval in seconds
|
136
|
+
lSecsInterval = getSecsInterval(iNotifierConf[:GroupReports])
|
137
|
+
# Maybe we don't want to send reports now
|
138
|
+
# Get the last time we sent reports for this one
|
139
|
+
if ((lDatabase[:LastReportsSent][iMonitorName] != nil) and
|
140
|
+
(lDatabase[:LastReportsSent][iMonitorName][iNotifierName] != nil) and
|
141
|
+
((Time.now.utc - lDatabase[:LastReportsSent][iMonitorName][iNotifierName]) < lSecsInterval))
|
142
|
+
# Reports from last one sent to the most recent one are marked to be sent later
|
143
|
+
lReportFiles.each do |iReportTime, iReportFileName|
|
144
|
+
if (iReportTime > lDatabase[:LastReportsSent][iMonitorName][iNotifierName])
|
145
|
+
# This report will be sent another time
|
146
|
+
lDelayedReports[iReportFileName] = nil
|
147
|
+
end
|
148
|
+
end
|
149
|
+
else
|
150
|
+
# Send all corresponding reports now
|
151
|
+
lLastReportSentDate = nil
|
152
|
+
if ((lDatabase[:LastReportsSent][iMonitorName] != nil) and
|
153
|
+
(lDatabase[:LastReportsSent][iMonitorName][iNotifierName] != nil))
|
154
|
+
lLastReportSentDate = lDatabase[:LastReportsSent][iMonitorName][iNotifierName]
|
155
|
+
else
|
156
|
+
lLastReportSentDate = Time.parse('1970-01-01 00:00:00 UTC')
|
157
|
+
end
|
158
|
+
lReportFilesToSend = []
|
159
|
+
lLastReportTime = Time.parse('1970-01-01 00:00:00 UTC')
|
160
|
+
lReportFiles.each do |iReportTime, iReportFileName|
|
161
|
+
if (iReportTime > lLastReportSentDate)
|
162
|
+
lReportFilesToSend << iReportFileName
|
163
|
+
lSentReports[iReportFileName] = nil
|
164
|
+
if (iReportTime > lLastReportTime)
|
165
|
+
lLastReportTime = iReportTime
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
if (!lReportFilesToSend.empty?)
|
170
|
+
# Send [iMonitorInfo, lReportFilesToSend] to iNotifierName
|
171
|
+
if (iNotifierConf[:GroupWithOtherMonitors] == true)
|
172
|
+
if (lGroupedMonitorReports[lNotifierID] == nil)
|
173
|
+
lGroupedMonitorReports[lNotifierID] = {}
|
174
|
+
end
|
175
|
+
if (lGroupedMonitorReports[lNotifierID][iMonitorName] == nil)
|
176
|
+
lGroupedMonitorReports[lNotifierID][iMonitorName] = []
|
177
|
+
end
|
178
|
+
lGroupedMonitorReports[lNotifierID][iMonitorName] << [ iNotifierConf, lReportFilesToSend ]
|
179
|
+
else
|
180
|
+
notify(iConf, {lNotifierID => {iMonitorName => [ [ iNotifierConf, lReportFilesToSend ] ]}}, lDelayedReports)
|
181
|
+
end
|
182
|
+
# Remember last report sent
|
183
|
+
if (lDatabase[:LastReportsSent][iMonitorName] == nil)
|
184
|
+
lDatabase[:LastReportsSent][iMonitorName] = {}
|
185
|
+
end
|
186
|
+
lDatabase[:LastReportsSent][iMonitorName][iNotifierName] = lLastReportTime
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
if (!lGroupedMonitorReports.empty?)
|
195
|
+
# Send all notifications that were grouped
|
196
|
+
notify(iConf, lGroupedMonitorReports, lDelayedReports)
|
197
|
+
end
|
198
|
+
# Now we can delete reports that were sent and are also not marked for delayed sending
|
199
|
+
lSentReports.each do |iReportFileName, iNil|
|
200
|
+
if (!lDelayedReports.has_key?(iReportFileName))
|
201
|
+
File.unlink(iReportFileName)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
# Log reports to be sent delayed
|
205
|
+
lDelayedReports.keys.each do |iReportFileName|
|
206
|
+
logInfo "Report to be sent later: #{iReportFileName}"
|
207
|
+
end
|
208
|
+
|
209
|
+
# Write back database
|
210
|
+
File.open(lDatabaseFileName, 'w') do |oFile|
|
211
|
+
oFile.write(Marshal.dump(lDatabase))
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
private
|
216
|
+
|
217
|
+
# Process notifications to be sent.
|
218
|
+
#
|
219
|
+
# Parameters:
|
220
|
+
# * *iConf* (<em>map<Symbol,Object></em>): SheepDog config
|
221
|
+
# * *iNotificationsInfo* (<em>map<NotifierName,map<MonitorName,list<[NotifierConf,list<ReportFileName>]>>></em>): The list of report files to send along with their notifier config, per monitor name, per notifier name
|
222
|
+
# * *ioErrorReports* (<em>map<ReportFileName,nil></em>): The set of report file names that could not be sent through notifications
|
223
|
+
def notify(iConf, iNotificationsInfo, ioErrorReports)
|
224
|
+
iNotificationsInfo.each do |iNotifierName, iNotifierNotificationsInfo|
|
225
|
+
# Find this notifier
|
226
|
+
if (iConf[:Notifiers][iNotifierName] == nil)
|
227
|
+
logErr "Unknown notifier named #{iNotifierName}. Ignoring notifications to be sent there. Please check configuration."
|
228
|
+
else
|
229
|
+
accessPlugin('Notifiers', iConf[:Notifiers][iNotifierName][:Type]) do |iNotifierPlugin|
|
230
|
+
# List of reports to send through this notifier
|
231
|
+
# list< Report >
|
232
|
+
lLstReports = []
|
233
|
+
# Set of report files that will be sent through this call
|
234
|
+
# map< ReportFileName, nil >
|
235
|
+
lReportFilesSet = {}
|
236
|
+
iNotifierNotificationsInfo.each do |iMonitorName, iLstMonitorNotificationsInfo|
|
237
|
+
iLstMonitorNotificationsInfo.each do |iMonitorNotificationsInfo|
|
238
|
+
iNotifierConf, iLstReportFileNames = iMonitorNotificationsInfo
|
239
|
+
iLstReportFileNames.each do |iReportFileName|
|
240
|
+
lReport = nil
|
241
|
+
begin
|
242
|
+
lReport = Marshal.load(File.read(iReportFileName))
|
243
|
+
rescue Exception
|
244
|
+
logErr "Invalid report stored in file #{iReportFileName}: #{$!}.\n#{$!.backtrace.join("\n")}"
|
245
|
+
ioErrorReports[iReportFileName] = nil
|
246
|
+
lReport = nil
|
247
|
+
end
|
248
|
+
if (lReport != nil)
|
249
|
+
lReport.setReportFileName(iReportFileName)
|
250
|
+
if (iNotifierConf[:Title] != nil)
|
251
|
+
lReport.setTitle(iNotifierConf[:Title])
|
252
|
+
end
|
253
|
+
lLstReports << lReport
|
254
|
+
lReportFilesSet[iReportFileName] = nil
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
begin
|
260
|
+
logInfo "===== Send notification to #{iNotifierName} of #{lLstReports.size} reports..."
|
261
|
+
iNotifierPlugin.sendNotification(iConf[:Notifiers][iNotifierName], lLstReports)
|
262
|
+
rescue Exception
|
263
|
+
logErr "Exception while sending notification from #{iNotifierName} for reports #{lReportFilesSet.keys.join(', ')}: #{$!}.\n#{$!.backtrace.join("\n")}"
|
264
|
+
ioErrorReports.merge!(lReportFilesSet)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Get the number of seconds defined in a configuration
|
272
|
+
#
|
273
|
+
# Parameters:
|
274
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The configuration
|
275
|
+
# Return:
|
276
|
+
# * _Fixnum_: The number of seconds
|
277
|
+
def getSecsInterval(iConf)
|
278
|
+
if (iConf[:Interval_Secs] != nil)
|
279
|
+
return iConf[:Interval_Secs]
|
280
|
+
else
|
281
|
+
logErr "Unable to decode interval from #{iConf.inspect}"
|
282
|
+
return 0
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
|
288
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module SheepDog
|
7
|
+
|
8
|
+
module Monitors
|
9
|
+
|
10
|
+
class LogFile
|
11
|
+
|
12
|
+
# Execute the monitoring process for a given configuration
|
13
|
+
#
|
14
|
+
# Parameters:
|
15
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The monitor configuration
|
16
|
+
def execute(iConf)
|
17
|
+
# Get past values
|
18
|
+
lReadValuesFileName = "#{@MonitorDir}/ReadValues"
|
19
|
+
lReadValues = nil
|
20
|
+
if (File.exists?(lReadValuesFileName))
|
21
|
+
lReadValues = Marshal.load(File.read(lReadValuesFileName))
|
22
|
+
else
|
23
|
+
lReadValues = {
|
24
|
+
:LastPos => 0,
|
25
|
+
:LastUpdate => Time.parse('1970-01-01 00:00:00 UTC')
|
26
|
+
}
|
27
|
+
end
|
28
|
+
if (File.exists?(iConf[:FileName]))
|
29
|
+
lUpdateTime = File.stat(iConf[:FileName]).mtime
|
30
|
+
if (lUpdateTime != lReadValues[:LastUpdate])
|
31
|
+
# The file was modified
|
32
|
+
# If the size is smaller, read from the beginning
|
33
|
+
lStartPos = nil
|
34
|
+
if (File.size(iConf[:FileName]) <= lReadValues[:LastPos])
|
35
|
+
lStartPos = 0
|
36
|
+
else
|
37
|
+
lStartPos = lReadValues[:LastPos]
|
38
|
+
end
|
39
|
+
# Read file
|
40
|
+
File.open(iConf[:FileName], 'r') do |iFile|
|
41
|
+
iFile.seek(lStartPos)
|
42
|
+
iFile.read.split("\n").each do |iLine|
|
43
|
+
# Match the line against filters
|
44
|
+
lMatch = false
|
45
|
+
iConf[:Filters].each do |iFilter|
|
46
|
+
if (iLine.match(iFilter) != nil)
|
47
|
+
lMatch = true
|
48
|
+
break
|
49
|
+
end
|
50
|
+
end
|
51
|
+
if (lMatch)
|
52
|
+
# Report this line
|
53
|
+
report iLine
|
54
|
+
end
|
55
|
+
end
|
56
|
+
lReadValues[:LastPos] = iFile.pos
|
57
|
+
end
|
58
|
+
lReadValues[:LastUpdate] = lUpdateTime
|
59
|
+
end
|
60
|
+
else
|
61
|
+
report "!!! Missing file #{iConf[:FileName]}"
|
62
|
+
lReadValues[:LastPos] = -1
|
63
|
+
end
|
64
|
+
# Write back read values
|
65
|
+
File.open(lReadValuesFileName, 'w') do |oFile|
|
66
|
+
oFile.write(Marshal.dump(lReadValues))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
require 'rUtilAnts/Misc'
|
7
|
+
RUtilAnts::Misc::initializeMisc
|
8
|
+
|
9
|
+
module SheepDog
|
10
|
+
|
11
|
+
module Monitors
|
12
|
+
|
13
|
+
class Process
|
14
|
+
|
15
|
+
# Execute the monitoring process for a given configuration
|
16
|
+
#
|
17
|
+
# Parameters:
|
18
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The monitor configuration
|
19
|
+
def execute(iConf)
|
20
|
+
# Get the list of processes
|
21
|
+
# list< Integer >
|
22
|
+
lLstPIDs = []
|
23
|
+
`ps -Af`.split("\n")[1..-1].map { |iLine| iLine.strip }.each do |iLine|
|
24
|
+
lMatch = iLine.match(/^(\S+)\s+(\d+)\s+\d+\s+\d+\s+\S+\s+\S+\s+\S+\s+(.+)$/)
|
25
|
+
if (lMatch == nil)
|
26
|
+
report "Unable to decode ps output: \"#{iLine}\". Ignoring this line."
|
27
|
+
else
|
28
|
+
lUser, lPID, lCmd = lMatch[1..3]
|
29
|
+
iConf[:Processes].each do |iProcessFilterInfo|
|
30
|
+
if (iProcessFilterInfo.empty?)
|
31
|
+
report 'Process filter info is empty. ignoring it. Please check configuration.'
|
32
|
+
else
|
33
|
+
lOut = false
|
34
|
+
if (iProcessFilterInfo.has_key?(:UserFilter))
|
35
|
+
lOut = (lUser.match(iProcessFilterInfo[:UserFilter]) == nil)
|
36
|
+
end
|
37
|
+
if ((!lOut) and
|
38
|
+
(iProcessFilterInfo.has_key?(:NameFilter)))
|
39
|
+
lOut = (lCmd.match(iProcessFilterInfo[:NameFilter]) == nil)
|
40
|
+
end
|
41
|
+
if (!lOut)
|
42
|
+
# This PID is selected
|
43
|
+
lLstPIDs << lPID.to_i
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
if (lLstPIDs.empty?)
|
50
|
+
# Maybe we want to execute something
|
51
|
+
if (iConf.has_key?(:ExecuteIfMissing))
|
52
|
+
report "Missing process. Executing \"#{iConf[:ExecuteIfMissing][:CmdLine]}\" from \"#{iConf[:ExecuteIfMissing][:Pwd]}\":"
|
53
|
+
changeDir(iConf[:ExecuteIfMissing][:Pwd]) do
|
54
|
+
report `#{iConf[:ExecuteIfMissing][:CmdLine]}`
|
55
|
+
end
|
56
|
+
end
|
57
|
+
elsif (iConf.has_key?(:Limits))
|
58
|
+
# Monitor the processes
|
59
|
+
# Set of PIDs exceeding limits
|
60
|
+
# map< Integer, nil >
|
61
|
+
lAboveLimitsPIDs = {}
|
62
|
+
lLstPIDs.each do |iPID|
|
63
|
+
lCPUPercent, lMemPercent, lVirtualSize = getPIDMetrics(iPID)
|
64
|
+
# Challenge metrics against limits
|
65
|
+
if ((iConf[:Limits].has_key?(:CPUPercent)) and
|
66
|
+
(lCPUPercent > iConf[:Limits][:CPUPercent]))
|
67
|
+
report "PID #{iPID} exceeds CPU percent limit: #{lCPUPercent} > #{iConf[:Limits][:CPUPercent]}"
|
68
|
+
lAboveLimitsPIDs[iPID] = nil
|
69
|
+
end
|
70
|
+
if ((iConf[:Limits].has_key?(:MemPercent)) and
|
71
|
+
(lMemPercent > iConf[:Limits][:MemPercent]))
|
72
|
+
report "PID #{iPID} exceeds Mem percent limit: #{lMemPercent} > #{iConf[:Limits][:MemPercent]}"
|
73
|
+
lAboveLimitsPIDs[iPID] = nil
|
74
|
+
end
|
75
|
+
if ((iConf[:Limits].has_key?(:VirtualMemSize)) and
|
76
|
+
(lVirtualSize > iConf[:Limits][:VirtualMemSize]))
|
77
|
+
report "PID #{iPID} exceeds virtual mem size limit: #{lVirtualSize} > #{iConf[:Limits][:VirtualMemSize]}"
|
78
|
+
lAboveLimitsPIDs[iPID] = nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
if (!lAboveLimitsPIDs.empty?)
|
82
|
+
# What to do with PIDs exceeding limits ?
|
83
|
+
if (iConf[:ActionAboveLimits] == :Kill)
|
84
|
+
# Kill them
|
85
|
+
report "Killing PIDs #{lAboveLimitsPIDs.keys.join(' ')} ..."
|
86
|
+
report `kill -9 #{lAboveLimitsPIDs.keys.join(' ')}`
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Get metrics of a PID
|
95
|
+
#
|
96
|
+
# Parameters:
|
97
|
+
# * *iPID* (_Integer_): The PID to get metrics from
|
98
|
+
# Return:
|
99
|
+
# * _Float_: The CPU percentage
|
100
|
+
# * _Float_: The mem percentage
|
101
|
+
# * _Integer_: The total virtual memory size
|
102
|
+
def getPIDMetrics(iPID)
|
103
|
+
rCPUPercent = nil
|
104
|
+
rMemPercent = nil
|
105
|
+
rVS = nil
|
106
|
+
|
107
|
+
# From top
|
108
|
+
lTopOutput = `top -n1 -p#{iPID} -b | tail -2 | head -1`.strip
|
109
|
+
lMatch = lTopOutput.match(/^\d+\s+\S+\s+\d+\s+\d+\s+\S+\s+\S+\s+\d+\s+\S+\s+(\S+)\s+(\S+)\s+\S+\s+.+$/)
|
110
|
+
if (lMatch == nil)
|
111
|
+
report "Unable to decode top output for PID #{iPID}: \"#{lTopOutput}\""
|
112
|
+
else
|
113
|
+
rCPUPercent, rMemPercent = lMatch[1..2].map { |iStrValue| iStrValue.to_f }
|
114
|
+
end
|
115
|
+
# From proc/<PID>/stat
|
116
|
+
lStatOutput = `cat /proc/#{iPID}/stat`.strip
|
117
|
+
lMatch = lStatOutput.match(/^\d+\s+\(.+\)\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)\s+/)
|
118
|
+
if (lMatch == nil)
|
119
|
+
report "Unable to decode stat output for PID #{iPID}: \"#{lStatOutput}\""
|
120
|
+
else
|
121
|
+
rVS = lMatch[1].to_i
|
122
|
+
end
|
123
|
+
|
124
|
+
return rCPUPercent, rMemPercent, rVS
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2010 - 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module SheepDog
|
7
|
+
|
8
|
+
module Notifiers
|
9
|
+
|
10
|
+
class SendMail
|
11
|
+
|
12
|
+
# Send notifications for a given list of reports.
|
13
|
+
#
|
14
|
+
# Parameters:
|
15
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The notifier config
|
16
|
+
# * *iLstReports* (<em>list<Report></em>): List of reports to notify
|
17
|
+
def sendNotification(iConf, iLstReports)
|
18
|
+
lTitle = nil
|
19
|
+
lMessage = nil
|
20
|
+
if (iLstReports.size > 1)
|
21
|
+
lTitle = "#{iLstReports.size} reports"
|
22
|
+
iLstReports.each_with_index do |iReport, iIdx|
|
23
|
+
lMessage << "===============================================\n"
|
24
|
+
lMessage << "========== Report #{iIdx+1} (#{iReport.CreationTime.utc.strftime('%Y-%m-%d %H:%M:%S')} UTC from #{iReport.ReportFileName}): #{iReport.Title}\n"
|
25
|
+
lMessage << iReport.getSimpleText
|
26
|
+
lMessage << "===============================================\n\n"
|
27
|
+
end
|
28
|
+
else
|
29
|
+
lReport = iLstReports.first
|
30
|
+
lTitle = "#{lReport.Title} (#{lReport.CreationTime.utc.strftime('%Y-%m-%d %H:%M:%S')} UTC from #{lReport.ReportFileName})"
|
31
|
+
lMessage = lReport.getSimpleText
|
32
|
+
end
|
33
|
+
require 'mail'
|
34
|
+
Mail.defaults do
|
35
|
+
delivery_method(:smtp, iConf[:SMTP])
|
36
|
+
end
|
37
|
+
Mail.deliver do
|
38
|
+
from iConf[:From]
|
39
|
+
to iConf[:To]
|
40
|
+
subject "SheepDog notification - #{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')} UTC - #{lTitle}"
|
41
|
+
body lMessage
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module SheepDog
|
7
|
+
|
8
|
+
module Notifiers
|
9
|
+
|
10
|
+
class StdOut
|
11
|
+
|
12
|
+
# Send notifications for a given list of reports.
|
13
|
+
#
|
14
|
+
# Parameters:
|
15
|
+
# * *iConf* (<em>map<Symbol,Object></em>): The notifier config
|
16
|
+
# * *iLstReports* (<em>list<Report></em>): List of reports to notify
|
17
|
+
def sendNotification(iConf, iLstReports)
|
18
|
+
lTitle = nil
|
19
|
+
lMessage = nil
|
20
|
+
if (iLstReports.size > 1)
|
21
|
+
lTitle = "#{iLstReports.size} reports"
|
22
|
+
iLstReports.each_with_index do |iReport, iIdx|
|
23
|
+
lMessage << "===============================================\n"
|
24
|
+
lMessage << "========== Report #{iIdx+1} (#{iReport.CreationTime.utc.strftime('%Y-%m-%d %H:%M:%S')} UTC from #{iReport.ReportFileName}): #{iReport.Title}\n"
|
25
|
+
lMessage << iReport.getSimpleText
|
26
|
+
lMessage << "===============================================\n\n"
|
27
|
+
end
|
28
|
+
else
|
29
|
+
lReport = iLstReports.first
|
30
|
+
lTitle = "#{lReport.Title} (#{lReport.CreationTime.utc.strftime('%Y-%m-%d %H:%M:%S')} UTC from #{lReport.ReportFileName})"
|
31
|
+
lMessage = lReport.getSimpleText
|
32
|
+
end
|
33
|
+
puts lTitle
|
34
|
+
puts
|
35
|
+
puts lMessage
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
#--
|
2
|
+
# Copyright (c) 2011 Muriel Salvan (murielsalvan@users.sourceforge.net)
|
3
|
+
# Licensed under the terms specified in LICENSE file. No warranty is provided.
|
4
|
+
#++
|
5
|
+
|
6
|
+
module SheepDog
|
7
|
+
|
8
|
+
# A report is a collection of entries, associated to a monitor run
|
9
|
+
class Report
|
10
|
+
|
11
|
+
# Title of the report
|
12
|
+
# String
|
13
|
+
attr_reader :Title
|
14
|
+
|
15
|
+
# File name of the report
|
16
|
+
# String
|
17
|
+
attr_reader :ReportFileName
|
18
|
+
|
19
|
+
# Time of this report's creation (UTC)
|
20
|
+
# Time
|
21
|
+
attr_reader :CreationTime
|
22
|
+
|
23
|
+
# Constructor
|
24
|
+
def initialize
|
25
|
+
@Entries = []
|
26
|
+
@CreationTime = Time.now
|
27
|
+
@ReportFileName = nil
|
28
|
+
@Title = nil
|
29
|
+
end
|
30
|
+
|
31
|
+
# Add an entry to the report
|
32
|
+
#
|
33
|
+
# Parameters:
|
34
|
+
# * *iEntry* (_String_): Entry to be added
|
35
|
+
def addEntry(iEntry)
|
36
|
+
@Entries << iEntry
|
37
|
+
end
|
38
|
+
|
39
|
+
# Set the report's title
|
40
|
+
#
|
41
|
+
# Parameters:
|
42
|
+
# * *iTitle* (_String_): Report's title
|
43
|
+
def setTitle(iTitle)
|
44
|
+
@Title = iTitle
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the report's file name
|
48
|
+
#
|
49
|
+
# Parameters:
|
50
|
+
# * *iFileName* (_String_): Report's file name
|
51
|
+
def setReportFileName(iFileName)
|
52
|
+
@ReportFileName = iFileName
|
53
|
+
end
|
54
|
+
|
55
|
+
# Get the report as simple text
|
56
|
+
#
|
57
|
+
# Return:
|
58
|
+
# * _String_: The report as simple text
|
59
|
+
def getSimpleText
|
60
|
+
return @Entries.join("\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Is this report empty ?
|
64
|
+
#
|
65
|
+
# Return:
|
66
|
+
# * _Boolean_: Is this report empty ?
|
67
|
+
def empty?
|
68
|
+
return @Entries.empty?
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
{
|
2
|
+
|
3
|
+
# The working directory, where SheepDog keeps reports waiting to be sent for notification and its private database
|
4
|
+
:WorkingDir => '/my/home/sheepdog_work',
|
5
|
+
|
6
|
+
# Define notifiers
|
7
|
+
:Notifiers => {
|
8
|
+
|
9
|
+
'Mail' => {
|
10
|
+
:Type => 'SendMail',
|
11
|
+
:SMTP => {
|
12
|
+
:address => 'localhost',
|
13
|
+
:port => 25,
|
14
|
+
:domain => 'mail.domain.com',
|
15
|
+
:user_name => 'smtpuser',
|
16
|
+
:password => 'password',
|
17
|
+
:authentication => nil,
|
18
|
+
:enable_starttls_auto => false
|
19
|
+
},
|
20
|
+
:From => 'SheepDog@domain.com',
|
21
|
+
:To => 'Admin@domain.com'
|
22
|
+
}
|
23
|
+
|
24
|
+
},
|
25
|
+
|
26
|
+
# Monitor
|
27
|
+
:Monitors => {
|
28
|
+
|
29
|
+
# Monitor production log file of Rails
|
30
|
+
'RailsLog' => {
|
31
|
+
:Type => 'LogFile',
|
32
|
+
:Notifiers => {
|
33
|
+
'Mail' => {
|
34
|
+
:Type => 'Mail',
|
35
|
+
:GroupReports => {
|
36
|
+
:Interval_Secs => 60*60*24
|
37
|
+
},
|
38
|
+
:Title => 'Rails production log file',
|
39
|
+
:GroupWithOtherMonitors => true
|
40
|
+
}
|
41
|
+
},
|
42
|
+
|
43
|
+
:FileName => '/my/home/rails/log/production.log',
|
44
|
+
:Filters => [
|
45
|
+
/Error/
|
46
|
+
]
|
47
|
+
},
|
48
|
+
|
49
|
+
# Monitor StatsCollect process
|
50
|
+
'StatsCollect' => {
|
51
|
+
:Type => 'Process',
|
52
|
+
:Notifiers => {
|
53
|
+
'Mail' => {
|
54
|
+
:Type => 'Mail',
|
55
|
+
:Title => 'StatsCollect process',
|
56
|
+
:GroupWithOtherMonitors => true
|
57
|
+
}
|
58
|
+
},
|
59
|
+
|
60
|
+
:Processes => [
|
61
|
+
{
|
62
|
+
:UserFilter => /username/,
|
63
|
+
:NameFilter => /StatsCollect\.rb/
|
64
|
+
}
|
65
|
+
],
|
66
|
+
:Limits => {
|
67
|
+
:CPUPercent => 5,
|
68
|
+
:MemPercent => 5,
|
69
|
+
:VirtualMemSize => 16777216
|
70
|
+
},
|
71
|
+
:ActionAboveLimits => :Kill
|
72
|
+
},
|
73
|
+
|
74
|
+
# Monitor the mongrel server
|
75
|
+
'Mongrel' => {
|
76
|
+
:Type => 'Process',
|
77
|
+
:Notifiers => {
|
78
|
+
'Mail' => {
|
79
|
+
:Type => 'Mail',
|
80
|
+
:Title => 'Mongrel server',
|
81
|
+
:GroupWithOtherMonitors => true
|
82
|
+
}
|
83
|
+
},
|
84
|
+
|
85
|
+
:Processes => [
|
86
|
+
{
|
87
|
+
:UserFilter => /mongreluser/,
|
88
|
+
:NameFilter => /mongrel_rails start/
|
89
|
+
}
|
90
|
+
],
|
91
|
+
:ExecuteIfMissing => {
|
92
|
+
:CmdLine => '/usr/bin/ruby /usr/bin/mongrel_rails start -p 12004 -d -e production -P log/mongrel.pid',
|
93
|
+
:Pwd => '/my/home/rails'
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
}
|
98
|
+
}
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: SheepDog
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 20110705
|
10
|
+
version: 0.1.0.20110705
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Muriel Salvan
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-05 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Simple command line tool that monitors files and processes and sends notifications or take corrective actions when problems arise. Monitor log files for errors, processes CPU and memory consumption (can kill if exceeding), respawn dead processes.
|
23
|
+
email: murielsalvan@users.sourceforge.net
|
24
|
+
executables:
|
25
|
+
- sheepdog.rb
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files: []
|
29
|
+
|
30
|
+
files:
|
31
|
+
- AUTHORS
|
32
|
+
- bin/sheepdog.rb
|
33
|
+
- ChangeLog
|
34
|
+
- Credits
|
35
|
+
- lib/sheepdog/Executor.rb
|
36
|
+
- lib/sheepdog/Monitors/LogFile.rb
|
37
|
+
- lib/sheepdog/Monitors/Process.rb
|
38
|
+
- lib/sheepdog/Notifiers/SendMail.rb
|
39
|
+
- lib/sheepdog/Notifiers/StdOut.rb
|
40
|
+
- lib/sheepdog/Report.rb
|
41
|
+
- LICENSE
|
42
|
+
- README
|
43
|
+
- ReleaseInfo
|
44
|
+
- sheepdog.conf.rb.example
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://sheepdogsys.sourceforge.net/
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
segments:
|
60
|
+
- 0
|
61
|
+
version: "0"
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
requirements: []
|
71
|
+
|
72
|
+
rubyforge_project: sheepdogsys
|
73
|
+
rubygems_version: 1.3.7
|
74
|
+
signing_key:
|
75
|
+
specification_version: 3
|
76
|
+
summary: System administration helper to monitor files and processes.
|
77
|
+
test_files: []
|
78
|
+
|