webstats 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 Brenton Fletcher (http://i.bloople.net i@bloople.net)
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,81 @@
1
+ h1. Webstats
2
+
3
+ Webstats is a server and clients that monitors your servers performance (CPU usage, memory usage, disk usage, disk activity, url load time) and allows you to get notifications on your computer when the server is having problems, as well as showing (http://kimag.es/share/91090734.png) the current performance stats for the server on a web page.
4
+
5
+ Webstats has 2 components: the _server_ and one or more _clients_.
6
+
7
+ h2. Server
8
+
9
+ The server runs on the computer whose performance you want to monitor; the server application's job is to (1) monitor the computer's performance and (2) report these performance statistics through a webserver that the server application runs.
10
+
11
+ See Installation below for install instructions.
12
+
13
+ The server application executes in the background and starts a tiny webserver on port 9970. You can view the performance stats for your server by going to http://<server's hostname>:9970/, for example if my server was under the domain name bloople.net, I would go to http://bloople.net:9970/. The statistics on this page automatically update every 5 seconds. You may need to open port 9970 if you have a firewall on your server. Note that the various clients get their data via the webserver the server application runs, just like the built-in stats page.
14
+
15
+ The statistic provided by the server application are:
16
+
17
+ * CPU usage and load average
18
+ * Memory (RAM) usage, with free, free - buffers, and total counts
19
+ * Disk usage by mount point
20
+ * Disk activity for reads and writes
21
+ * URL monitoring, to load URL's you specify to ensure they work
22
+
23
+ The server application will warn you of any of these statistics indicate a problem on the server - for example if you've have < 5MB free RAM for the last 12 seconds, that indicates a potential problem on your server that needs looking at. If you're looking at the performance statistics web page, the warnings will be presented by higlighting the perf stat that's causing the warning; when you're using a client application, it will present warnings to you as it sees fit.
24
+
25
+ You can configure at what thresholds the warnings get generated, as well as the URL's to monitor, by editing ~/.webstats. This file is generated the first time you run webstats, so it's pre-filled with sensible defaults; but you can change them as you see fit.
26
+
27
+ You can prevent public access to your webstats by invoking the server application with a single argument (e.g. <code>ruby webstats.rb <password to use></code>). Then, when you visit the webstats, you'll be prompted for a username and password; the username is always 'webstats', and the password is the password specified just above.
28
+
29
+ The server application requires Linux kernel 2.6+, will not work in *BSD or OS X; the server application requires only Ruby; it does not rely on rubygems or any external libraries that aren't included with Ruby; the server application uses very little RAM, approximately 10MB.
30
+
31
+ All the client applications depend on the server application being run to work.
32
+
33
+ h2. Clients
34
+
35
+ If all you want to do is be able to view the performance statistics for your server in a web browser, then you don't need to use any of these clients; going to http://&lt;server's hostname&gt;:9970/ will do just fine. But if you want to have Growl or email notification when something goes wrong, without having to look at a web page all the time, then you'll want one of the client applications that come with Webstats.
36
+
37
+ There are currently two client applications available:
38
+
39
+ * An email notifier; documentation is available in clients/email_notifier/README.textile
40
+ * A Growl notifier; documentation is available in clients/growl_notifier/README.textile
41
+
42
+ The notifiers that ship with Webstats can be started in 2 ways; if you have installed webstats via rubygems, run:
43
+
44
+ <pre><code>~$ webstats_<notifier_name></code></pre>
45
+
46
+ For example, to run the email notifier, you can run webstats_email_notifier. If you installed webstats outside of rubygems, first change directory into the directory containing webstats, then run:
47
+ <pre><code>~/webstats$ ruby clients/<notifier_name>/<notifier_name>.rb</code></pre>
48
+
49
+ Many notifiers can also be run on the server, in the same ruby process as teh webstats server itself. To do this, change any settings as required by the documentation for the notifier, then add the notifier name (e.g. email_notifier) to the list of clients under webstats > clients in the ~/.webstats file
50
+
51
+ h2. Installation
52
+
53
+ You can now install webstats using rubygems; this saves you a bit of hassle. To install webstats on the server via rubygems, ensure github gems are in your gems sources; then run:
54
+ <pre><code>~$ sudo gem install bloopletech-webstats</code></pre>
55
+
56
+ To run the server application, run:
57
+ <pre><code>~$ webstats</code></pre>
58
+
59
+ The server application will start in the background.
60
+
61
+ If you don't want to use rubygems, then follow the below instructions:
62
+ To install the server app, ssh into the computer you want to monitor. then run:
63
+ <pre><code>~$ git clone git://github.com/bloopletech/webstats.git
64
+ ~$ cd webstats
65
+ ~/webstats$ cd server/data_providers
66
+ ~/webstats/server/data_providers$ ruby extconf.rb
67
+ ~/webstats/server/data_providers$ make
68
+ </code></pre>
69
+
70
+ To run the server app, ssh into the computer you want to monitor. then run:
71
+ <pre><code>~$ cd webstats
72
+ ~/webstats$ ruby server/webstats.rb
73
+ </code></pre>
74
+
75
+ h2. Todo
76
+
77
+ * More client applications.
78
+ * Extend server to work on *BSD (including OS X).
79
+ * Make webstats server optionally install itself to run at boot.
80
+ * Make client applications optionally install themselves at boot, where suitable.
81
+ * (Maybe) Move all server code into it's own module and make code work without running the web server, so you can use it to just grab stats in your own code.
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |s|
6
+ s.name = %q{webstats}
7
+ s.version = "0.1.0"
8
+
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.authors = ["Brenton Fletcher"]
11
+ s.date = Date.today.strftime("%Y-%m-%d")
12
+ s.description = s.summary = %q{Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.}
13
+ s.email = %q{i@bloople.net}
14
+ s.files = Dir['**/*'].reject { |fn| fn =~ /(\.o|\.so|\.bundle|Makefile|\.gem)$/ }
15
+ s.executables = ['webstats', 'webstats_growl_notifier', 'webstats_email_notifier']
16
+ s.extensions = ["server/data_providers/extconf.rb"]
17
+ s.has_rdoc = false
18
+ s.homepage = %q{http://github.com/bloopletech/webstats}
19
+ s.require_paths = [""]
20
+ end
21
+ rescue LoadError
22
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
23
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 10
4
+ :patch: 5
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/../server/webstats'
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/../clients/email_notifier/email_notifier'
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/../clients/growl_notifier/growl_notifier'
@@ -0,0 +1,13 @@
1
+ h1. Email notifier for Webstats
2
+
3
+ The email notifier montiors one or more servers and emails you whenever the server is in danger or could be in danger soon - for example, running out of hard drive space.
4
+
5
+ h2. Usage
6
+
7
+ Run the notifier by running webstats_email_notifier or ruby <webstats_install_dir>/clients/email_notifier/email_notifier.rb on the terminal.
8
+
9
+ It'll create a template of the settings you'll need to change in ~/.webstats_client - you'll need to set the recipient address for the emails, you may need to change the mail server settings - you can supply these settings: address, port, domain, username, password, authentication (one of :plain, :login, or :cram_md5).
10
+
11
+ You'll also need to add some URL's for the email notifier to monitor - these are the URL's who statistics will be monitored. You will be notified when there is a warning or danger situation for a URL, as well as if one of the URL's can not be loaded. The URL's should the the hostnames of the servers you want to monitor, along with the correct port number (e.g. http://bloople.net:9970/).
12
+
13
+ Once that's done, run the notifier again and it will begin monitoring.
@@ -0,0 +1,66 @@
1
+ require File.dirname(__FILE__) + '/../simple_notifier'
2
+ daemonize!
3
+
4
+ class EmailNotifier < SimpleNotifier
5
+ def initialize(settings = {}, read_config = true)
6
+ @name = 'email_notifier'
7
+ @no_settings_message = "Please edit ~/.webstats_clients and add some URLs to monitor and an email address to notfiy on"
8
+
9
+ super({ 'recipient' => '', 'mail_server' => { 'address' => 'localhost', 'domain' => 'localhost', 'port' => 25 } }.merge(settings), read_config)
10
+ end
11
+
12
+ private
13
+ def send_mail(subject, message)
14
+ sm = @settings[:mail_server]
15
+
16
+ msg = <<END_OF_MESSAGE
17
+ From: Webstats Email Notifier <#{@settings[:recipient]}>
18
+ To: #{@settings[:recipient]} <#{@settings[:recipient]}>
19
+ Subject: #{subject}
20
+ Date: #{Time.now.rfc2822}
21
+ Message-Id: <#{Time.now.to_i}.#{rand(10000000)}@#{sm[:domain]}>
22
+
23
+ #{message}
24
+ END_OF_MESSAGE
25
+
26
+ Net::SMTP.start(sm[:address], sm[:port], sm[:domain], sm[:username], sm[:password], sm[:authentication]) { |smtp| smtp.send_message msg, @settings[:recipient], @settings[:recipient] }
27
+ end
28
+
29
+ def failed_url(url, password, exception)
30
+ send_mail("Webstats Notification - Cannot load Webstats data", "Could not load #{url}#{!password.nil? ? " with password #{password}" : ""}, error was #{exception.message}. Will try again in 60 seconds.")
31
+ sleep(60)
32
+ true
33
+ end
34
+
35
+ def notify
36
+ messages = []
37
+ has_warnings = has_dangers = false
38
+
39
+ @settings[:urls].each do |url|
40
+ if !url[:bad].empty? and (url[:changed] or url[:time_past])
41
+ has_warnings = !url[:warnings].empty?
42
+ has_dangers = !url[:dangers].empty?
43
+
44
+ title = []
45
+ title << "Danger" if has_dangers
46
+ title << "Warnings" if has_warnings
47
+ title = title.join(" & ")
48
+
49
+ warnings_text = !url[:warnings].empty? ? "Warnings for #{url[:bad].select { |(k, v)| v['status'] == 'warning' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
50
+ danger_text = !url[:dangers].empty? ? "Dangerous situation for #{url[:bad].select { |(k, v)| v['status'] == 'danger' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
51
+
52
+ host = URI.parse(url[:url]).host
53
+ messages << "#{title} for #{host}\n#{"-" * (title.length + host.length + 5)}\n\n#{[danger_text, warnings_text].compact.join("\n")}\n\nCheck statistics online at #{url[:url]}"
54
+ end
55
+ end
56
+
57
+ title = []
58
+ title << "Danger" if has_dangers
59
+ title << "Warnings" if has_warnings
60
+
61
+ send_mail("Webstats Notification - #{title.join(" & ")}", messages.join("\n\n\n")) unless messages.empty?
62
+ end
63
+ end
64
+
65
+ s = EmailNotifier.new()
66
+ s.start
@@ -0,0 +1,125 @@
1
+ ### Copyright
2
+ #
3
+ # Copyright 2004 Thomas Kollbach <dev@bitfever.de>
4
+ #
5
+ # Released under the BSD license.
6
+ #
7
+ ### Description
8
+ #
9
+ # A ruby class that enables posting notifications to the Growl daemon.
10
+ # See <http://growl.info> for more information.
11
+ #
12
+ # Requires RubyCocoa (http://www.fobj.com/rubycocoa/) and Ruby 1.8
13
+ # (http://ruby-lang.org).
14
+ #
15
+ ### Versions
16
+ #
17
+ # v0.1- 25.11.2004 - Initial version, this is less more then a ruby translation of
18
+ # the python bindings
19
+ #
20
+ # TODO: transform this into a ruby-module, so it is usable as a mixin
21
+ # for ruby-scripts
22
+ #
23
+ ### Usage
24
+ #
25
+ # Here is a short example how to use this in a script
26
+ #
27
+ # n = GrowlNotifier.new('bla',['Foo'],nil,OSX::NSWorkspace.sharedWorkspace().iconForFileType_('unknown'))
28
+ # n.register()
29
+ #
30
+ # n.notify('Foo', 'Test Notification', 'Blah blah blah')
31
+ #
32
+ ###
33
+
34
+ require 'osx/cocoa'
35
+
36
+ $priority = {"Very Low" => -2,
37
+ "Moderate" => -1,
38
+ "Normal" => 0,
39
+ "High" => 1,
40
+ "Emergency"=> 2
41
+ }
42
+
43
+
44
+ class GrowlNotifier
45
+ # A class that abstracts the process of registering and posting
46
+ # notifications to the Growl daemon.
47
+ #
48
+ # `appName': The name of the application
49
+ # `notifications': an array of notifications - default is an empty array
50
+ #
51
+ # `defaultNotifications': optional - defaults to the value of
52
+ # `notifications'
53
+ # `appIcon' is also optional but defaults to a senseless icon so
54
+ # so you are higly encouraged to pass it along.
55
+ #
56
+
57
+ def initialize(appName='GrowlNotifier', notifications=[], defaultNotifications=nil, appIcon=nil)
58
+ @appName = appName
59
+ @notifications = notifications
60
+ @defaultNotifications = defaultNotifications
61
+ @appIcon = appIcon
62
+ end #initialize
63
+
64
+ def register
65
+ if @appIcon == nil then
66
+ @appIcon = OSX::NSWorkspace.sharedWorkspace().iconForFileType_("txt")
67
+ end
68
+ if @defaultNotifications == nil then
69
+ @defaultNotifications = @notifications
70
+ end
71
+
72
+ regData = {
73
+ 'ApplicationName'=>@appName,
74
+ 'AllNotifications'=> OSX::NSArray.arrayWithArray(@notifications),
75
+ 'DefaultNotifications'=> OSX::NSArray.arrayWithArray(@defaultNotifications),
76
+ 'ApplicationIcon'=> @appIcon.TIFFRepresentation
77
+ }
78
+
79
+ dict = OSX::NSDictionary.dictionaryWithDictionary(regData)
80
+ notifyCenter = OSX::NSDistributedNotificationCenter.defaultCenter
81
+
82
+ notifyCenter.postNotificationName_object_userInfo_deliverImmediately_("GrowlApplicationRegistrationNotification", nil, dict, true)
83
+ end #register
84
+
85
+ def notify(noteType, title, description, icon=nil, appIcon=nil, sticky=false, priority=nil)
86
+ # Post a notification to the Growl daemon.
87
+ #
88
+ # `noteType' is the name of the notification that is being posted.
89
+ # `title' is the user-visible title for this notification.
90
+ # `description' is the user-visible description of this notification.
91
+ # `icon' is an optional icon for this notification. It defaults to
92
+ # `@applicationIcon'.
93
+ # `appIcon' is an optional icon for the sending application.
94
+ # `sticky' is a boolean controlling whether the notification is sticky.
95
+
96
+
97
+ @notifications << noteType
98
+ if icon == nil then icon = @appIcon end
99
+
100
+ notification = {'NotificationName'=> noteType,
101
+ 'ApplicationName'=> @appName,
102
+ 'NotificationTitle'=> title,
103
+ 'NotificationDescription'=> description,
104
+ 'NotificationIcon'=> icon.TIFFRepresentation()}
105
+
106
+ unless appIcon == nil
107
+ notification['NotificationAppIcon'] = appicon.TIFFRepresentation
108
+ end
109
+
110
+ if sticky
111
+ notification['NotificationSticky'] = OSX::NSNumber.numberWithBool_(true)
112
+ end
113
+
114
+ unless priority == nil
115
+ notification['NotificationPriority'] = OSX::NSNumber.numberWithInt_(priority)
116
+ end
117
+
118
+ d = OSX::NSDictionary.dictionaryWithDictionary_(notification)
119
+
120
+ notCenter = OSX::NSDistributedNotificationCenter.defaultCenter()
121
+ notCenter.postNotificationName_object_userInfo_deliverImmediately_('GrowlNotification', nil, d, true)
122
+
123
+ end #notify
124
+ end #class growlnotifier
125
+
@@ -0,0 +1,13 @@
1
+ h1. Growl notifer for Webstats
2
+
3
+ The Growl notifier runs in the background and monitors the server; if the server goes under high CPU load for more than a few seconds, or if the server is nearly out of memory, you'll get a Growl notification saying what the problem is. This way, you can just set and forget, knowing that Webstats will report any problems.
4
+
5
+ Thy works on OS X only, with RubyCocoa installed.
6
+
7
+ .h2 Usage
8
+
9
+ Run the notifier by running webstats_growl_notifier or ruby <webstats_install_dir>/clients/growl_notifier/growl_notifier.rb on the terminal.
10
+
11
+ The first time you run the growl notifier, it will create a template of the settings you need to edit at ~/.webstats_clients; you must edit this file to set the URL's for the growl notifier to monitor. The URL's should the the hostnames of the servers you want to monitor, along with the correct port number (e.g. http://bloople.net:9970/).
12
+
13
+ Once you've edited the configuration file to add your URL's, run the notifier again; the Growl notifier will then run in the background and notify you od any warnings or danger situations on the server.
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + '/../simple_notifier'
2
+ daemonize!
3
+ require File.dirname(__FILE__) + '/Growl.rb'
4
+
5
+ class WGrowlNotifier < SimpleNotifier
6
+ def initialize(settings = {}, read_config = true)
7
+ @name = "growl_notifier"
8
+ @no_settings_message = "Please edit ~/.webstats_clients and add some URLs to monitor"
9
+
10
+ @g = GrowlNotifier.new("Webstats", ['Webstats Notification'], nil, OSX::NSWorkspace.sharedWorkspace().iconForFileType_('unknown'))
11
+ @g.register
12
+
13
+ super(settings, read_config)
14
+ end
15
+
16
+ private
17
+ def failed_url(url, password, exception)
18
+ @g.notify "Webstats Notification", "Cannot load Webstats data", "Could not load #{url}#{!password.nil? ? " with password #{password}" : ""}, error was #{exception.message}. Will try again in 60 seconds."
19
+ sleep(60)
20
+ true
21
+ end
22
+
23
+ def notify
24
+ @settings[:urls].each do |url|
25
+ if !url[:bad].empty? and (url[:changed] or url[:time_past])
26
+ title = []
27
+ title << "Danger" unless url[:dangers].empty?
28
+ title << "Warnings" unless url[:warnings].empty?
29
+ title = title.join(" & ") + " for host #{URI.parse(url[:url]).host}"
30
+
31
+ warnings_text = !url[:warnings].empty? ? "Warnings for #{url[:bad].select { |(k, v)| v['status'] == 'warning' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
32
+ danger_text = !url[:dangers].empty? ? "Dangerous situation for #{url[:bad].select { |(k, v)| v['status'] == 'danger' }.map { |(k, v)| url[:meta_info][k]['in_sentence'] }.join(", ")}." : nil
33
+
34
+ @g.notify "Webstats Notification", title, [danger_text, warnings_text].compact.join(" "), nil, nil, true, (!url[:dangers].empty? ? 2 : 1)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ s = WGrowlNotifier.new()
41
+ s.start
@@ -0,0 +1,106 @@
1
+ require 'yaml'
2
+ require 'net/http'
3
+ require 'uri'
4
+ require 'thread'
5
+ require 'time'
6
+ require 'net/smtp'
7
+
8
+ def daemonize!
9
+ return if $DAEMONIZE == false
10
+ if $DEBUG
11
+ Thread.abort_on_exception
12
+ else
13
+ if (pid = fork)
14
+ Signal.trap('HUP', 'IGNORE')
15
+ Process.detach(pid)
16
+ exit
17
+ end
18
+ end
19
+ end
20
+
21
+ class Hash
22
+ alias_method :undecorated_get, :[] unless method_defined?(:undecorated_get)
23
+ def [](key)
24
+ undecorated_get(key) or undecorated_get(key.is_a?(String) ? key.to_sym : key.to_s)
25
+ end
26
+ end
27
+
28
+ class SimpleNotifier
29
+ def initialize(settings = {}, read_config = true)
30
+ @name ||= "notifier"
31
+ @default_settings ||= {}
32
+
33
+ if read_config
34
+ config_file_path = File.expand_path("~/.webstats_clients")
35
+
36
+ @settings = {}
37
+
38
+ if File.exists?(config_file_path)
39
+ @settings = YAML.load(IO.read(config_file_path))
40
+ @settings ||= {}
41
+ end
42
+
43
+ unless @settings.key?(@name)
44
+ @settings[@name] = { 'urls' => [{ 'url' => 'http://localhost:9970/', 'password' => nil }] }.merge(@default_settings).merge(settings)
45
+ File.open(config_file_path, "w") { |f| YAML.dump(@settings, f) }
46
+
47
+ puts @no_settings_message
48
+ exit
49
+ end
50
+
51
+ @settings = @settings[@name]
52
+ else
53
+ @settings = settings
54
+ end
55
+ end
56
+
57
+ def start
58
+ @settings[:urls].each do |url|
59
+ url[:mutex] = Mutex.new
60
+ Thread.new do
61
+ url[:mutex].synchronize { url.merge!({ :meta_info => make_request(URI.join(url[:url], "information"), url[:password]), :last_time => 0 }) }
62
+ end
63
+ end
64
+
65
+ while(true)
66
+ threads = []
67
+ @settings[:urls].each do |url|
68
+ threads << Thread.new do
69
+ url[:mutex].synchronize do
70
+ url[:data] = make_request(URI.join(url[:url], "update"), url[:password])
71
+ url[:bad] = url[:data].sort { |a, b| b[1]['importance'].to_f <=> a[1]['importance'].to_f }.select { |(k, v)| !v['status'].nil? && v['status'] != '' }
72
+ url[:last_warnings] = url[:warnings] || []
73
+ url[:warnings] = url[:bad].select { |(k, v)| v['status'] == 'warning' }
74
+ url[:has_warnings]= !url[:warnings].empty?
75
+ url[:last_dangers] = url[:dangers] || []
76
+ url[:dangers] = url[:bad].select { |(k, v)| v['status'] == 'danger' }
77
+ url[:has_dangers] = !url[:dangers].empty?
78
+ url[:changed] = (!url[:warnings].empty? || !url[:dangers].empty?) && (!url.key?(:changed) or (url[:warnings].length > url[:last_warnings].length) or (url[:dangers].length > url[:last_dangers].length))
79
+ url[:time_past] = url[:last_time] != 0 && (Time.now - url[:last_time]) > 60
80
+ url[:last_time] = Time.now if url[:changed] || url[:last_time].nil? || url[:time_past]
81
+ end
82
+ end
83
+ end
84
+ threads.each { |t| t.join }
85
+ notify
86
+ sleep(10)
87
+ end
88
+ end
89
+
90
+ private
91
+ def make_request(url, password)
92
+ while(true)
93
+ begin
94
+ Net::HTTP.start(url.host, url.port) { |http|
95
+ http.read_timeout = http.open_timeout = 15
96
+ req = Net::HTTP::Get.new(url.request_uri)
97
+ req.basic_auth 'webstats', password unless password.nil?
98
+ return YAML.load(http.request(req).body)
99
+ }
100
+ rescue Exception => e
101
+ return nil unless failed_url(url, password, e)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
@@ -0,0 +1,74 @@
1
+ class DataProviders::CpuInfo
2
+ def initialize(settings)
3
+ @settings = self.class.default_settings.merge(settings)
4
+
5
+ @readings = []
6
+ @mutex = Mutex.new
7
+
8
+ @thread = Thread.new do
9
+ last_time = last_user = last_nice = last_system = last_idle = last_iowait = 0
10
+ first_time = true
11
+ while(true)
12
+ time = (Time.new.to_f * 1000).to_i
13
+
14
+ user, nice, system, idle, iowait, crap = IO.readlines("/proc/stat").first.split(' ', 6).map { |i| i.to_i }
15
+
16
+ if first_time
17
+ first_time = false
18
+ else
19
+ temp_user = (user - last_user)
20
+ temp_nice = (nice - last_nice)
21
+ temp_system = (system - last_system)
22
+ temp_idle = (idle - last_idle)
23
+ temp_iowait = (iowait - last_iowait)
24
+
25
+ @mutex.synchronize do
26
+ @readings.unshift((temp_user + temp_nice + temp_system) / (temp_idle.to_f < 1 ? 1 : temp_idle.to_f))
27
+ @readings.pop while @readings.length > 5
28
+ end
29
+ end
30
+ last_user = user
31
+ last_nice = nice
32
+ last_system = system
33
+ last_idle = idle
34
+ last_iowait = iowait
35
+ last_time = time
36
+ sleep(@settings[:update_rate])
37
+ end
38
+ end
39
+ end
40
+
41
+ def get
42
+ out = { :usage => 0 }
43
+ @mutex.synchronize do
44
+ unless @readings.empty?
45
+ out[:usage] = @readings.first
46
+ out[:status] = 'warning' unless @readings.detect { |r| out[:usage] < @settings[:usage_warning_level] }
47
+ out[:status] = 'danger' unless @readings.detect { |r| out[:usage] < @settings[:usage_danger_level] }
48
+ end
49
+ end
50
+ out[:loadavg_1], out[:loadavg_5], out[:loadavg_15] = IO.readlines("/proc/loadavg").first.split(' ', 4).map { |v| v.to_f }
51
+ out
52
+ end
53
+
54
+ def renderer
55
+ information.merge({ :contents => %{
56
+ sc.innerHTML = "<div class='major_figure'><span class='title'>Usage</span><span class='figure'>" + data_source['usage'] + "</span><span class='unit'>%</span></div>" +
57
+ "<div class='major_figure'><span class='title'>Load average</span><span class='figure'>" + data_source['loadavg_1'] +
58
+ "</span><span class='unit'>1m</span><span class='divider'>/</span><span class='figure'>" + data_source['loadavg_5'] +
59
+ "</span><span class='unit'>5m</span><span class='divider'>/</span><span class='figure'>" + data_source['loadavg_15'] + "</span><span class='unit'>15m</span></div>";
60
+ } })
61
+ end
62
+
63
+ def self.default_settings
64
+ { :update_rate => 2.5, :usage_warning_level => 95, :usage_danger_level => 99.5 }
65
+ end
66
+
67
+ def information
68
+ { :name => "CPU Info", :in_sentence => 'CPU load', :importance => 100 }
69
+ end
70
+
71
+ def kill
72
+ @thread.kill
73
+ end
74
+ end
@@ -0,0 +1,52 @@
1
+ class DataProviders::DiskActivity
2
+ def initialize(settings)
3
+ @settings = self.class.default_settings.merge(settings)
4
+
5
+ @reads_sec = 0
6
+ @writes_sec = 0
7
+
8
+ @thread = Thread.new do
9
+ last_time = last_reads = last_writes = 0
10
+ first_time = true
11
+ while(true)
12
+ time = (Time.new.to_f * 1000).to_i
13
+
14
+ reads, writes = IO.readlines("/proc/diskstats").map { |l| parts = l.split; [parts[5].to_i, parts[9].to_i] }.inject([0, 0]) { |sum, vals| [sum[0] + vals[0], sum[1] + vals[1]] }
15
+
16
+ if first_time
17
+ first_time = false
18
+ else
19
+ @reads_sec = ((reads - last_reads) / ((time - last_time).to_f / 1000.0)) * 512
20
+ @writes_sec = ((writes - last_writes) / ((time - last_time).to_f / 1000.0)) * 512
21
+ end
22
+ last_reads = reads
23
+ last_writes = writes
24
+ last_time = time
25
+ sleep(@settings[:update_rate])
26
+ end
27
+ end
28
+ end
29
+
30
+ def get
31
+ { :reads => @reads_sec / 1024.0 / 1024.0, :writes => @writes_sec / 1024.0 / 1024.0 }
32
+ end
33
+
34
+ def renderer
35
+ information.merge({ :contents => %{
36
+ sc.innerHTML = "<div class='major_figure'><span class='title'>Reads</span><span class='figure'>" + data_source['reads'] + "</span><span class='unit'>mb/s</span></div>" +
37
+ "<div class='major_figure'><span class='title'>Writes</span><span class='figure'>" + data_source['writes'] + "</span><span class='unit'>mb/s</span></div>";
38
+ } })
39
+ end
40
+
41
+ def self.default_settings
42
+ { :update_rate => 2.5 }
43
+ end
44
+
45
+ def information
46
+ { :name => "Disk Activity", :in_sentence => "Disk Activity", :importance => 70 }
47
+ end
48
+
49
+ def kill
50
+ @thread.kill
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ #include <ruby.h>
2
+ #include <sys/statvfs.h>
3
+
4
+ static VALUE get_disk_usage(VALUE self, VALUE mount_point)
5
+ {
6
+ char* mount_point_str = RSTRING(mount_point)->ptr;
7
+
8
+ VALUE out_hash = rb_hash_new();
9
+ rb_hash_aset(out_hash, rb_str_new2("free"), Qnil);
10
+ rb_hash_aset(out_hash, rb_str_new2("total"), Qnil);
11
+
12
+ struct statvfs result;
13
+
14
+ if(statvfs(mount_point_str, &result) == 0)
15
+ {
16
+ rb_hash_aset(out_hash, rb_str_new2("free"), LL2NUM((long long)result.f_bavail * (long long)result.f_frsize));
17
+ rb_hash_aset(out_hash, rb_str_new2("total"), LL2NUM((long long)result.f_blocks * (long long)result.f_frsize));
18
+ }
19
+
20
+ return out_hash;
21
+ }
22
+
23
+ void Init_disk_usage()
24
+ {
25
+ VALUE mDataProviders = rb_const_get(rb_cObject, rb_intern("DataProviders"));
26
+ VALUE cDiskUsage = rb_const_get(mDataProviders, rb_intern("DiskUsage"));
27
+
28
+ rb_define_method(cDiskUsage, "get_disk_usage", get_disk_usage, 1);
29
+ }
@@ -0,0 +1,54 @@
1
+ class DataProviders::DiskUsage
2
+ def initialize(settings)
3
+ @settings = self.class.default_settings.merge(settings)
4
+ end
5
+
6
+ def get
7
+ out = { :mounts => [] }
8
+
9
+ mtab = IO.readlines("/etc/mtab").sort_by { |l| l.split[1] }
10
+ mtab.map do |mp|
11
+ parts = mp.split
12
+ next unless parts[3].split(",").detect { |p| p == "rw" }
13
+ du = get_disk_usage(parts[1])
14
+ out[:mounts] << [parts[1], du] unless du['total'] == 0 or (du['total'] > 5242880 && ((du['total'] - du['free']) <= 1048576))
15
+ end
16
+
17
+ out[:mounts].map do |mp|
18
+ mp[1]['free'] /= (1024.0 * 1024)
19
+ mp[1]['total'] /= (1024.0 * 1024)
20
+ out[:status] = "warning" if mp[1]['free'] < @settings[:warning_threshold] and mp[1]['total'] > @settings[:warning_minimum_mount_point_size] and out[:status] != 'danger'
21
+ out[:status] = "danger" if mp[1]['free'] < @settings[:danger_threshold] and mp[1]['total'] > @settings[:danger_minimum_mount_point_size]
22
+ end
23
+
24
+ out
25
+ end
26
+
27
+ def renderer
28
+ information.merge({ :contents => %{
29
+ var temp = "";
30
+ for(var i = 0; i < data_source['mounts'].length; i++)
31
+ {
32
+ var mpd = data_source['mounts'][i][1];
33
+ temp += "<div class='major_figure'><span class='title'>" + data_source['mounts'][i][0] + "</span><span class='figure'>" + mpd['free'] +
34
+ "</span><span class='unit'>mb free</span><span class='divider'>/</span><span class='figure'>" + mpd['total'] +
35
+ "</span><span class='unit'>mb total</span></div>";
36
+ }
37
+
38
+ sc.innerHTML = temp;
39
+ } })
40
+ end
41
+
42
+ def self.default_settings
43
+ { :warning_threshold => 50, :danger_threshold => 10, :warning_minimum_mount_point_size => 100, :danger_minimum_mount_point_size => 20 }
44
+ end
45
+
46
+ def information
47
+ { :name => "Disk Usage by Mount Point", :in_sentence => "Disk Usage", :importance => 80 }
48
+ end
49
+
50
+ def kill
51
+ end
52
+ end
53
+
54
+ require File.dirname(__FILE__) + '/disk_usage.so'
@@ -0,0 +1,5 @@
1
+ require 'mkmf'
2
+
3
+ dir_config("c_extensions")
4
+
5
+ create_makefile("disk_usage")
@@ -0,0 +1,54 @@
1
+ class DataProviders::MemInfo
2
+ def initialize(settings)
3
+ @settings = self.class.default_settings.merge(settings)
4
+
5
+ @readings = []
6
+ @mutex = Mutex.new
7
+
8
+ @thread = Thread.new do
9
+ while(true)
10
+ out = {}
11
+ out[:total], out[:free], out[:buffers], out[:cached] = IO.readlines("/proc/meminfo")[0..4].map { |l| l =~ /^.*?\: +(.*?) kB$/; $1.to_i / 1024.0 }
12
+ out[:free_total] = out[:free] + out[:buffers] + out[:cached]
13
+
14
+ @mutex.synchronize do
15
+ @readings.unshift(out)
16
+ @readings.pop while @readings.length > 5
17
+ end
18
+ sleep(@settings[:update_rate])
19
+ end
20
+ end
21
+ end
22
+
23
+ def get
24
+ out = { :total => 0, :free => 0, :buffers => 0, :cached => 0, :free_total => 0 }
25
+ @mutex.synchronize do
26
+ unless @readings.empty?
27
+ out = @readings.first.dup
28
+ out[:status] = 'warning' unless @readings.detect { |r| r[:free] > 5 }
29
+ out[:status] = 'danger' unless @readings.detect { |r| r[:free_total] > 1 }
30
+ end
31
+ end
32
+ out
33
+ end
34
+
35
+ def renderer
36
+ information.merge({ :contents => %{
37
+ sc.innerHTML = "<div class='major_figure'><span class='title'>Free</span><span class='figure'>" + data_source['free'] + "</span><span class='unit'>mb</span></div>" +
38
+ "<div class='major_figure'><span class='title'>Free -buffers/cache</span><span class='figure'>" + data_source['free_total'] + "</span><span class='unit'>mb</span></div>" +
39
+ "<div class='major_figure'><span class='title'>Total</span><span class='figure'>" + data_source['total'] + "</span><span class='unit'>mb</span></div>";
40
+ } })
41
+ end
42
+
43
+ def self.default_settings
44
+ { :update_rate => 2.5 }
45
+ end
46
+
47
+ def information
48
+ { :name => "Memory Info", :in_sentence => 'Memory Usage', :importance => 90 }
49
+ end
50
+
51
+ def kill
52
+ @thread.kill
53
+ end
54
+ end
@@ -0,0 +1,71 @@
1
+ require 'net/http'
2
+
3
+ class DataProviders::UrlMonitor
4
+ def initialize(settings)
5
+ @settings = self.class.default_settings.merge(settings)
6
+
7
+ @readings = {}
8
+
9
+ @mutex = Mutex.new
10
+
11
+ @thread = Thread.new do
12
+ while(true)
13
+ @settings[:urls].sort.each do |url|
14
+ @mutex.synchronize { @readings[url] = { :response_time => -1, :works => (@readings.key?(url) && @readings[url][:works] == :failed ? :failed : :waiting) } }
15
+ duration = 0
16
+ works = :failed
17
+ begin
18
+ uri = URI.parse(url)
19
+ start = Time.now
20
+ Net::HTTP.start(uri.host, uri.port) { |http|
21
+ http.read_timeout = http.open_timeout = @settings[:danger_response_time_threshold]
22
+ raise Exception unless [200, 201, 202, 203, 204, 205, 206, 301, 302, 304].include? http.get(uri.path).code.to_i
23
+ }
24
+
25
+ duration = Time.now - start
26
+ works = :works
27
+ rescue Exception => e
28
+ end
29
+ @mutex.synchronize { @readings[url] = { :response_time => duration * 1000, :works => works } }
30
+ end
31
+ sleep(@settings[:update_rate])
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ def get
38
+ out = {}
39
+ @mutex.synchronize { out[:urls] = @readings.to_a.sort_by { |e| e[0] } }
40
+ out[:urls].each do |(url, info)|
41
+ out[:status] = 'warning' if (info[:works] == :failed) or info[:response_time] > @settings[:warning_response_time_threshold] * 1000 and !out[:status] == 'danger'
42
+ out[:status] = 'danger' if (info[:works] == :failed) or info[:response_time] > @settings[:danger_response_time_threshold] * 1000
43
+ end
44
+ out
45
+ end
46
+
47
+ def renderer
48
+ information.merge({ :contents => %{
49
+ var temp = "";
50
+ for(var i = 0; i < data_source['urls'].length; i++)
51
+ {
52
+ var ud = data_source['urls'][i][1];
53
+ temp += "<div class='major_figure'><span class='title'>" + data_source['urls'][i][0] + "</span><span class='figure'>" +
54
+ (ud['works'] == 'failed' ? 'Failed</span>' : (ud['works'] == 'waiting' ? 'Waiting</span>' : ud['response_time'] + "</span><span class='unit'>ms</span>")) + "</div>";
55
+ }
56
+
57
+ sc.innerHTML = temp;
58
+ } })
59
+ end
60
+
61
+ def self.default_settings
62
+ { :update_rate => 30, :warning_response_time_threshold => 5, :danger_response_time_threshold => 15, :urls => ['http://localhost/'] }
63
+ end
64
+
65
+ def information
66
+ { :name => "URL Monitor", :in_sentence => "URL Monitor", :importance => 60 }
67
+ end
68
+
69
+ def kill
70
+ end
71
+ end
@@ -0,0 +1,269 @@
1
+ #NO GEM DEPENDENCIES FTW
2
+ require 'webrick'
3
+ require 'yaml'
4
+
5
+ if $DEBUG
6
+ Thread.abort_on_exception
7
+ else
8
+ if (pid = fork)
9
+ Signal.trap('HUP', 'IGNORE')
10
+ Process.detach(pid)
11
+ exit
12
+ end
13
+ end
14
+
15
+ Thread.new do
16
+ while(true)
17
+ sleep(300)
18
+ GC.start
19
+ end
20
+ end
21
+
22
+ class NilClass
23
+ def to_json; "null"; end
24
+ end
25
+
26
+ class TrueClass
27
+ def to_json; "true"; end
28
+ end
29
+
30
+ class FalseClass
31
+ def to_json; "false"; end
32
+ end
33
+
34
+ class String
35
+ def underscore
36
+ self.gsub(/::/, '/').
37
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
38
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
39
+ tr("-", "_").
40
+ downcase
41
+ end
42
+ alias_method :to_json, :inspect
43
+ end
44
+
45
+ class Numeric
46
+ def formatted(precision = 1)
47
+ rounded_number = (Float(self) * (10 ** precision)).round.to_f / 10 ** precision
48
+ parts = ("%01.#{precision}f" % rounded_number).to_s.split('.')
49
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
50
+ parts.join(".")
51
+ end
52
+ alias_method :to_json, :inspect
53
+ end
54
+
55
+ class Array
56
+ def formatted!
57
+ each_with_index do |v, i|
58
+ if v.is_a? Numeric
59
+ self[i] = v.formatted
60
+ elsif v.is_a? Hash or v.is_a? Array
61
+ self[i] = self[i].dup.formatted!
62
+ end
63
+ end
64
+ end
65
+
66
+ def symbolize_keys!
67
+ each_with_index { |v, i| self[i] = self[i].dup.symbolize_keys! if v.is_a? Hash }
68
+ end
69
+
70
+ def stringify_keys!
71
+ each_with_index { |v, i| self[i] = self[i].dup.stringify_keys! if v.is_a? Hash }
72
+ end
73
+
74
+ def to_json
75
+ "[#{map { |e| e.to_json }.join(', ')}]"
76
+ end
77
+ end
78
+
79
+ class Hash
80
+ def formatted!
81
+ each_pair do |k, v|
82
+ if v.is_a? Numeric
83
+ self[k] = v.formatted
84
+ elsif v.is_a? Hash or v.is_a? Array
85
+ self[k] = self[k].dup.formatted!
86
+ end
87
+ end
88
+ end
89
+
90
+ def symbolize_keys!
91
+ keys.each { |key| self[key.to_sym] = delete(key) }
92
+ each_pair { |k, v| self[k] = self[k].dup.symbolize_keys! if v.is_a? Hash }
93
+ end
94
+
95
+ def stringify_keys!
96
+ keys.each { |key| self[key.to_s] = delete(key) }
97
+ each_pair { |k, v| self[k] = self[k].dup.stringify_keys! if v.is_a? Hash }
98
+ end
99
+
100
+ alias_method :undecorated_get, :[] unless method_defined?(:undecorated_get)
101
+ def [](key)
102
+ undecorated_get(key) or undecorated_get(key.is_a?(String) ? key.to_sym : key.to_s)
103
+ end
104
+
105
+ def to_json
106
+ arr = []
107
+ each_pair { |k, v| arr << "#{k.to_json}: #{v.to_json}" }
108
+ "{#{arr.join(', ')}}"
109
+ end
110
+ end
111
+
112
+ class Symbol
113
+ def to_json
114
+ to_s.inspect
115
+ end
116
+ end
117
+
118
+ module DataProviders
119
+ DATA_SOURCES_CLASSES = {}
120
+ DATA_SOURCES = {}
121
+ def self.preload
122
+ Dir.glob("#{File.dirname(__FILE__)}/data_providers/*.rb").each { |file| load file unless file =~ /extconf.rb$/ }
123
+ DataProviders.constants.each do |c|
124
+ c = DataProviders.const_get(c)
125
+ DATA_SOURCES_CLASSES[c.to_s.gsub(/^DataProviders::/, '').underscore] = c if c.is_a? Class
126
+ end
127
+ end
128
+ def self.setup(settings)
129
+ DATA_SOURCES_CLASSES.each_pair { |k, v| DATA_SOURCES[k] = v.new(settings[k]) }
130
+ end
131
+ end
132
+
133
+ DataProviders.preload
134
+
135
+ WEBSTATS_PATH = File.expand_path("~/.webstats")
136
+
137
+ $settings = {}
138
+
139
+ if File.exists?(WEBSTATS_PATH)
140
+ $settings = YAML.load(IO.read(WEBSTATS_PATH)).symbolize_keys!
141
+ else
142
+ $settings['webstats'] = { 'password' => nil, 'clients' => [] }
143
+ DataProviders::DATA_SOURCES_CLASSES.each_pair { |k, v| $settings[k.to_s] = v.default_settings.stringify_keys! }
144
+ File.open(WEBSTATS_PATH, "w") { |f| YAML.dump($settings, f) }
145
+ end
146
+
147
+ DataProviders.setup($settings)
148
+
149
+ class Webstats < WEBrick::HTTPServlet::AbstractServlet
150
+ def do_GET(req, res)
151
+ WEBrick::HTTPAuth.basic_auth(req, res, "Webstats") { |u, p| u == 'webstats' and p == $settings[:webstats][:password] } unless $settings[:webstats][:password].nil?
152
+
153
+ body = ""
154
+ if req.path_info == '/'
155
+ body << <<-EOF
156
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
157
+ <html>
158
+ <head>
159
+ <title>Webstats</title>
160
+ <style type="text/css">
161
+ * { margin: 0; padding: 0; font-family: "Lucida Grande", Helvetica, Arial, sans-serif; font-size: 100%; }
162
+ body { font-size: 95%; }
163
+ p { margin: 0 0 1em 0; }
164
+
165
+ h1 { margin: 1em; }
166
+ h1 span { font-size: 160%; font-weight: bold; }
167
+ .source .danger { background-color: #FF0D33; }
168
+ .source .warning { background-color: #F1FF28; }
169
+
170
+ .source { width: 500px; border: 1px solid #000000; margin: 1em; }
171
+ .source h2 { padding: 0 0.8em 0 0.8em; background-color: #C98300; }
172
+ .source h2 span { font-size: 130%; font-weight: bold; padding: 0.2em 0; display: block; }
173
+ .source .source_contents { padding: 0.8em; }
174
+ .source .title { padding-right: 0.5em; }
175
+ .source .major_figure { font-size: 130%; margin: 0.3em 0; }
176
+ .source .major_figure .figure { font-size: 120%; font-weight: bold; font-family: Georgia, serif; }
177
+ .source .major_figure .unit { font-family: Georgia, serif; font-size: 70%; }
178
+ .source .minor_figure { font-family: Georgia, serif; }
179
+ .source .divider { margin-left: 0.2em; margin-right: 0.2em; font-weight: normal; }
180
+ </style>
181
+ <script type="text/javascript">
182
+ var http = null;
183
+
184
+ function getLatest()
185
+ {
186
+ http.open("get", "/update", true);
187
+ http.send(null);
188
+ }
189
+
190
+ window.onload = function()
191
+ {
192
+ http = !!(window.attachEvent && !window.opera) ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
193
+
194
+ http.onreadystatechange = function()
195
+ {
196
+ if(http.readyState == 4)
197
+ {
198
+ var results = eval("(" + http.responseText + ")");
199
+ if(!results) return;
200
+ EOF
201
+
202
+ DataProviders::DATA_SOURCES.each_pair do |k, v|
203
+ body << %{var data_source = results['#{k}']; var sc = document.getElementById('source_contents_#{k}'); sc.className = "source_contents " + (data_source['status'] ? data_source['status'] : ''); #{v.renderer[:contents]}\n}
204
+ end
205
+
206
+ body << <<-EOF
207
+ }
208
+ };
209
+
210
+ window.setInterval("getLatest()", 5000);
211
+ getLatest();
212
+ }
213
+ </script>
214
+ </head>
215
+ <body id="body">
216
+ <div id="main">
217
+ <h1><span>Stats for #{req.host}</span></h1>
218
+ EOF
219
+ DataProviders::DATA_SOURCES.sort { |a, b| b[1].information[:importance] <=> a[1].information[:importance] }.each do |(k, v)|
220
+ r = v.renderer
221
+ body << %{<div class="source" id="source_#{k}"><h2><span>#{r[:name]}</span></h2><div class="source_contents" id="source_contents_#{k}">Loading...</div></div>}
222
+ end
223
+
224
+ body << <<-EOF
225
+ </div>
226
+ </body>
227
+ </html>
228
+ EOF
229
+ elsif req.path_info == '/update'
230
+ out = {}
231
+ DataProviders::DATA_SOURCES.each_pair do |k, v|
232
+ out[k] = v.get.dup
233
+ end
234
+
235
+ out.formatted!
236
+
237
+ body << out.to_json
238
+ elsif req.path_info == '/information'
239
+ out = {}
240
+ DataProviders::DATA_SOURCES.each_pair { |k, v| out[k] = v.information }
241
+ body << out.to_json
242
+ end
243
+
244
+ res.body = body
245
+ res['Content-Type'] = "text/html"
246
+ end
247
+ end
248
+
249
+ threads = []
250
+
251
+ s = WEBrick::HTTPServer.new(:Port => 9970, :Logger => WEBrick::Log.new(nil, 0), :AccessLog => WEBrick::Log.new(nil, 0))
252
+
253
+ death = proc do
254
+ s.shutdown
255
+ DataProviders::DATA_SOURCES.each_pair { |k, v| v.kill }
256
+ threads.each { |t| t.kill }
257
+ end
258
+ trap("INT", death)
259
+ trap("TERM", death)
260
+
261
+ s.mount("/", Webstats)
262
+ threads << Thread.new { s.start }
263
+
264
+ if $settings['webstats'].key?(:clients) and !$settings['webstats'][:clients].nil?
265
+ $DAEMONIZE = false
266
+ $settings['webstats'][:clients].each { |name| threads << Thread.new { require "#{File.dirname(__FILE__)}/../clients/#{name}/#{name}.rb" } }
267
+ end
268
+
269
+ threads.first.join
@@ -0,0 +1,57 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{webstats}
5
+ s.version = "0.10.5"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Brenton Fletcher"]
9
+ s.date = %q{2009-06-27}
10
+ s.description = %q{Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.}
11
+ s.email = %q{i@bloople.net}
12
+ s.executables = ["webstats", "webstats_growl_notifier", "webstats_email_notifier"]
13
+ s.extensions = ["server/data_providers/extconf.rb"]
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.textile"
17
+ ]
18
+ s.files = [
19
+ "LICENSE",
20
+ "README.textile",
21
+ "Rakefile",
22
+ "VERSION.yml",
23
+ "bin/webstats",
24
+ "bin/webstats_email_notifier",
25
+ "bin/webstats_growl_notifier",
26
+ "clients/email_notifier/README.textile",
27
+ "clients/email_notifier/email_notifier.rb",
28
+ "clients/growl_notifier/Growl.rb",
29
+ "clients/growl_notifier/README.textile",
30
+ "clients/growl_notifier/growl_notifier.rb",
31
+ "clients/simple_notifier.rb",
32
+ "server/data_providers/cpu_info.rb",
33
+ "server/data_providers/disk_activity.rb",
34
+ "server/data_providers/disk_usage.c",
35
+ "server/data_providers/disk_usage.rb",
36
+ "server/data_providers/extconf.rb",
37
+ "server/data_providers/mem_info.rb",
38
+ "server/data_providers/url_monitor.rb",
39
+ "server/webstats.rb",
40
+ "webstats.gemspec"
41
+ ]
42
+ s.homepage = %q{http://github.com/bloopletech/webstats}
43
+ s.rdoc_options = ["--charset=UTF-8"]
44
+ s.require_paths = [""]
45
+ s.rubygems_version = %q{1.3.1}
46
+ s.summary = %q{Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.}
47
+
48
+ if s.respond_to? :specification_version then
49
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
50
+ s.specification_version = 2
51
+
52
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
53
+ else
54
+ end
55
+ else
56
+ end
57
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: webstats
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Brenton Fletcher
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-25 00:00:00 +10:30
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.
22
+ email: i@bloople.net
23
+ executables:
24
+ - webstats
25
+ - webstats_growl_notifier
26
+ - webstats_email_notifier
27
+ extensions:
28
+ - server/data_providers/extconf.rb
29
+ extra_rdoc_files:
30
+ - LICENSE
31
+ - README.textile
32
+ files:
33
+ - LICENSE
34
+ - README.textile
35
+ - Rakefile
36
+ - VERSION.yml
37
+ - bin/webstats
38
+ - bin/webstats_email_notifier
39
+ - bin/webstats_growl_notifier
40
+ - clients/email_notifier/README.textile
41
+ - clients/email_notifier/email_notifier.rb
42
+ - clients/growl_notifier/Growl.rb
43
+ - clients/growl_notifier/README.textile
44
+ - clients/growl_notifier/growl_notifier.rb
45
+ - clients/simple_notifier.rb
46
+ - server/data_providers/cpu_info.rb
47
+ - server/data_providers/disk_activity.rb
48
+ - server/data_providers/disk_usage.c
49
+ - server/data_providers/disk_usage.rb
50
+ - server/data_providers/extconf.rb
51
+ - server/data_providers/mem_info.rb
52
+ - server/data_providers/url_monitor.rb
53
+ - server/webstats.rb
54
+ - webstats.gemspec
55
+ has_rdoc: true
56
+ homepage: http://github.com/bloopletech/webstats
57
+ licenses: []
58
+
59
+ post_install_message:
60
+ rdoc_options:
61
+ - --charset=UTF-8
62
+ require_paths:
63
+ - ""
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 0
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.6
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Monitor server CPU/Memory/Disk Usage/URL Loading, so that you can view those statistics on a web page, as well as providing an interface to client prorams to read those statistics.
85
+ test_files: []
86
+