bloopletech-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.
data/README ADDED
@@ -0,0 +1,71 @@
1
+ Webstats
2
+ ========
3
+
4
+ Webstats is a server and clients that monitors your servers performance (CPU usage, memory usage, disk usage) 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.
5
+
6
+ Webstats has 2 components: the _server_ and one or more _clients_.
7
+
8
+ Server
9
+ ------
10
+ 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 app runs.
11
+
12
+ See Installation below for install instructions.
13
+
14
+ The server app 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/ to see my performance stats. 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 app runs, just like the built-in stats page.
15
+
16
+ You can prevent public access to your webstats by invoking the server application with a single argument (e.g. ruby webstats.rb <password>). 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.
17
+
18
+ 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 7MB.
19
+
20
+ All the client applications depend on the server application being run to work.
21
+
22
+ Clients
23
+ -------
24
+ If all you want to do is be able to view the performance stats for your server in a web browser, then you don't need to use any of these clients; going to http://<server's hostname>: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.
25
+
26
+ Right now there's only one client application available, which is a Growl notifier. The Growl notifier only works on OS X, with RubyCocoa installed. This application 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.
27
+
28
+ To run the Growl notifier, open a terminal on your computer, then run (the first line is only needed the first time you run the notifier):
29
+ ~$ git clone git://github.com/bloopletech/webstats.git
30
+ ~$ cd webstats/clients/growl_notifier
31
+ ~/webstats/clients/growl_notifier$ ruby growl_notifier.rb http://<server's hostname>:9970/
32
+
33
+ Where server's hostname is the host name of the server you want to monitor (e.g. bloople.net). The Growl notifier will then run in the background.
34
+
35
+ Installation
36
+ ------------
37
+
38
+ 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:
39
+ ~$ sudo gem install bloopletech-webstats
40
+
41
+ To run the server application, run:
42
+ ~$ webstats
43
+
44
+ The server application will start in the background.
45
+
46
+ Coming soon: growl notifier client via rubygems.
47
+
48
+ If you don't want to use rubygems, then follow the below instructions:
49
+ To install the server app, ssh into the computer you want to monitor. then run:
50
+ ~$ git clone git://github.com/bloopletech/webstats.git
51
+ ~$ cd webstats
52
+ ~/webstats$ cd server/data_providers
53
+ ~/webstats/server/data_providers$ ruby extconf.rb
54
+ ~/webstats/server/data_providers$ make
55
+
56
+ To run the server app, ssh into the computer you want to monitor. then run:
57
+ ~$ cd webstats
58
+ ~/webstats$ ruby server/webstats.rb
59
+
60
+ Todo
61
+ ----
62
+ * (DONE) (Coming soon) Add disk space remaining, with warnings / danger signals.
63
+ * (DONE) (Coming soon) Add optional authentication to server application.
64
+ * (Coming soon) Make webstats configurable.
65
+ * Add email notifier to client applications.
66
+ * More client applications.
67
+ * Extend server to work on *BSD (including OS X).
68
+ * Make webstats server optionally install itself to run at boot.
69
+ * Make client applications optionally install themselves at boot, where suitable.
70
+ * (DONE) Make into a gem?
71
+ * (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.
data/Rakefile ADDED
@@ -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 = %q{2009-04-22}
12
+ s.description = s.summary = %q{Display server CPU/Memory/Disk Usage on a web page, suitable for remote performance monitoring.}
13
+ s.email = %q{i@bloople.net}
14
+ s.files = Dir['**/*']
15
+ s.executables = ['webstats']
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
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/bin/webstats ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/../server/webstats'
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'net/smtp'
3
+ require 'net/dns/resolver'
4
+ require 'net/dns/rr'
5
+ require 'time'
6
+
7
+ def send_mail_hardcore(recipient, recipient_name, from, from_name, subject, message)
8
+ username, domain = recipient.split('@', 2) #yeah yeah, username can contain @ sign, will fix later
9
+
10
+ mxrs = Net::DNS::Resolver.new.mx(domain)
11
+ if mxrs.empty?
12
+ puts "No MX records on domain; bad domain name"
13
+ return
14
+ end
15
+
16
+ msg = <<END_OF_MESSAGE
17
+ From: #{from_name || recipient_name} <#{from || recipient}>
18
+ To: #{recipient_name} <#{recipient}>
19
+ Subject: #{subject}
20
+ Date: #{Time.now.rfc2822}
21
+ Message-Id: <#{Time.now.to_i}.#{rand(10000000)}@#{domain}>
22
+
23
+ #{message}
24
+ END_OF_MESSAGE
25
+
26
+ Net::SMTP.start(mxrs.first.exchange, 25, domain) { |smtp| smtp.send_message msg, recipient, recipient }
27
+ end
28
+
29
+ send_mail_hardcore(*ARGV)
@@ -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,45 @@
1
+ exit if fork
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+ require 'net/http'
6
+ require 'uri'
7
+ require 'Growl'
8
+
9
+ url = ARGV[0]
10
+
11
+ g = GrowlNotifier.new("Webstats for #{url}",['Webstats Notification'], nil, OSX::NSWorkspace.sharedWorkspace().iconForFileType_('unknown'))
12
+ g.register
13
+
14
+ meta_info = JSON.parse(Net::HTTP.get(URI.join(url, "information")))
15
+ last_warnings_text = last_danger_text = nil
16
+ last_time = 0
17
+
18
+ while(true)
19
+ data = JSON.parse(Net::HTTP.get(URI.join(url, "update")))
20
+
21
+ bad = data.sort { |a, b| b[1]['importance'].to_f <=> a[1]['importance'].to_f }.select { |(k, v)| !v['status'].nil? && v['status'] != '' }
22
+
23
+ has_warnings = bad.detect { |(k, v)| v['status'] == 'warning' }
24
+ has_dangers = bad.detect { |(k, v)| v['status'] == 'danger' }
25
+
26
+ title = []
27
+ title << "Danger" if has_dangers
28
+ title << "Warnings" if has_warnings
29
+ title = title.join(" & ") + " for host #{URI.parse(url).host}"
30
+
31
+ warnings_text = has_warnings ? "Warnings for #{bad.select { |(k, v)| v['status'] == 'warning' }.map { |(k, v)| meta_info[k]['in_sentence'] }.join(", ")}." : nil
32
+ danger_text = has_dangers ? "Dangerous situation for #{bad.select { |(k, v)| v['status'] == 'danger' }.map { |(k, v)| meta_info[k]['in_sentence'] }.join(", ")}." : nil
33
+
34
+ if last_warnings_text != warnings_text or danger_text != last_danger_text or (Time.now - last_time) > 60
35
+ last_warnings_text = warnings_text
36
+ last_danger_text = danger_text
37
+ last_time = Time.now
38
+
39
+ unless bad.empty?
40
+ g.notify "Webstats Notification", title, [danger_text, warnings_text].compact.join(" "), nil, nil, true, (has_dangers ? 2 : 1)
41
+ end
42
+ end
43
+
44
+ sleep(10)
45
+ end
@@ -0,0 +1,72 @@
1
+ class DataProviders::CpuInfo
2
+ def initialize
3
+ @readings = []
4
+ @mutex = Mutex.new
5
+
6
+ @thread = Thread.new do
7
+ last_time = last_user = last_nice = last_system = last_idle = last_iowait = 0
8
+ first_time = true
9
+ while(true)
10
+ time = (Time.new.to_f * 1000).to_i
11
+
12
+ user, nice, system, idle, iowait, crap = IO.readlines("/proc/stat").first.split(' ', 6).map { |i| i.to_i }
13
+
14
+ if first_time
15
+ first_time = false
16
+ else
17
+ temp_user = (user - last_user)
18
+ temp_nice = (nice - last_nice)
19
+ temp_system = (system - last_system)
20
+ temp_idle = (idle - last_idle)
21
+ temp_iowait = (iowait - last_iowait)
22
+
23
+ @mutex.synchronize do
24
+ @readings.unshift((temp_user + temp_nice + temp_system) / (temp_idle.to_f < 1 ? 1 : temp_idle.to_f))
25
+ @readings.pop while @readings.length > 5
26
+ end
27
+ end
28
+ last_user = user
29
+ last_nice = nice
30
+ last_system = system
31
+ last_idle = idle
32
+ last_iowait = iowait
33
+ last_time = time
34
+ sleep(2.5)
35
+ end
36
+ end
37
+ end
38
+
39
+ def get
40
+ out = { :usage => 0 }
41
+ @mutex.synchronize do
42
+ unless @readings.empty?
43
+ out[:usage] = @readings.first
44
+ out[:status] = 'warning' unless @readings.detect { |r| out[:usage] < 95 }
45
+ out[:status] = 'danger' unless @readings.detect { |r| out[:usage] < 99.5 }
46
+ end
47
+ end
48
+ out[:loadavg_1], out[:loadavg_5], out[:loadavg_15] = IO.readlines("/proc/loadavg").first.split(' ', 4).map { |v| v.to_f }
49
+ out
50
+ end
51
+
52
+ def renderer
53
+ information.merge({ :contents => %{
54
+ sc.innerHTML = "<div class='major_figure'><span class='title'>Usage</span><span class='figure'>" + data_source['usage'] + "</span><span class='unit'>%</span></div>" +
55
+ "<div class='major_figure'><span class='title'>Load average</span><span class='figure'>" + data_source['loadavg_1'] +
56
+ "</span><span class='unit'>1m</span><span class='divider'>/</span><span class='figure'>" + data_source['loadavg_5'] +
57
+ "</span><span class='unit'>5m</span><span class='divider'>/</span><span class='figure'>" + data_source['loadavg_15'] + "</span><span class='unit'>15m</span></div>";
58
+ } })
59
+ end
60
+
61
+ def information
62
+ { :name => "CPU Info", :in_sentence => 'CPU load', :importance => importance }
63
+ end
64
+
65
+ def importance
66
+ 100
67
+ end
68
+
69
+ def kill
70
+ @thread.kill
71
+ end
72
+ end
@@ -0,0 +1,50 @@
1
+ class DataProviders::DiskActivity
2
+ def initialize
3
+ @reads_sec = 0
4
+ @writes_sec = 0
5
+
6
+ @thread = Thread.new do
7
+ last_time = last_reads = last_writes = 0
8
+ first_time = true
9
+ while(true)
10
+ time = (Time.new.to_f * 1000).to_i
11
+
12
+ reads, writes = IO.readlines("/proc/diskstats").map { |l| parts = l.split; [parts[5].to_i, parts[7].to_i] }.inject([0, 0]) { |sum, vals| [sum[0] + vals[0], sum[1] + vals[1]] }
13
+
14
+ if first_time
15
+ first_time = false
16
+ else
17
+ @reads_sec = ((reads - last_reads) / ((time - last_time).to_f / 1000.0)) * 512
18
+ @writes_sec = ((writes - last_writes) / ((time - last_time).to_f / 1000.0)) * 512
19
+ end
20
+ last_reads = reads
21
+ last_writes = writes
22
+ last_time = time
23
+ sleep(2.5)
24
+ end
25
+ end
26
+ end
27
+
28
+ def get
29
+ { :reads => @reads_sec / 1024.0, :writes => @writes_sec / 1024.0 }
30
+ end
31
+
32
+ def renderer
33
+ information.merge({ :name => "Disk Activity", :in_sentence => "Disk Activity", :importance => importance, :contents => %{
34
+ 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>" +
35
+ "<div class='major_figure'><span class='title'>Writes</span><span class='figure'>" + data_source['writes'] + "</span><span class='unit'>mb/s</span></div>";
36
+ } })
37
+ end
38
+
39
+ def information
40
+ { :name => "Disk Activity", :in_sentence => "Disk Activity", :importance => importance }
41
+ end
42
+
43
+ def importance
44
+ 70
45
+ end
46
+
47
+ def kill
48
+ @thread.kill
49
+ end
50
+ end
@@ -0,0 +1,29 @@
1
+ #include <ruby.h>
2
+ #include <sys/vfs.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 statfs result;
13
+
14
+ if(statfs(mount_point_str, &result) == 0)
15
+ {
16
+ rb_hash_aset(out_hash, rb_str_new2("free"), LL2NUM((long long)result.f_bfree * (long long)result.f_bsize));
17
+ rb_hash_aset(out_hash, rb_str_new2("total"), LL2NUM((long long)result.f_blocks * (long long)result.f_bsize));
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,53 @@
1
+ class DataProviders::DiskUsage
2
+ def initialize
3
+ end
4
+
5
+ def get
6
+ out = { :mounts => [] }
7
+
8
+ mtab = IO.readlines("/etc/mtab").sort_by { |l| l.split[1] }
9
+ mtab.map do |mp|
10
+ parts = mp.split
11
+ next unless parts[3].split(",").detect { |p| p == "rw" }
12
+ du = get_disk_usage(parts[1])
13
+ out[:mounts] << [parts[1], du] unless du['total'] == 0 or (du['total'] > 5242880 && ((du['total'] - du['free']) <= 1048576))
14
+ end
15
+
16
+ out[:mounts].map do |mp|
17
+ mp[1]['free'] /= (1024.0 * 1024)
18
+ mp[1]['total'] /= (1024.0 * 1024)
19
+ out[:status] = "warning" if mp[1]['free'] < 50 and mp[1]['total'] > 100 and out[:status] != 'danger'
20
+ out[:status] = "danger" if mp[1]['free'] < 10 and mp[1]['total'] > 20
21
+ end
22
+
23
+ out
24
+ end
25
+
26
+ def renderer
27
+ information.merge({ :name => "Disk Usage by Mount Point", :in_sentence => "Disk Usage", :importance => importance, :contents => %{
28
+ var temp = "";
29
+ for(var i = 0; i < data_source['mounts'].length; i++)
30
+ {
31
+ var mpd = data_source['mounts'][i][1];
32
+ temp += "<div class='major_figure'><span class='title'>" + data_source['mounts'][i][0] + "</span><span class='figure'>" + mpd['free'] +
33
+ "</span><span class='unit'>mb free</span><span class='divider'>/</span><span class='figure'>" + mpd['total'] +
34
+ "</span><span class='unit'>mb total</span></div>";
35
+ }
36
+
37
+ sc.innerHTML = temp;
38
+ } })
39
+ end
40
+
41
+ def information
42
+ { :name => "Disk Usage by Mount Point", :in_sentence => "Disk Usage", :importance => importance }
43
+ end
44
+
45
+ def importance
46
+ 80
47
+ end
48
+
49
+ def kill
50
+ end
51
+ end
52
+
53
+ 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,52 @@
1
+ class DataProviders::MemInfo
2
+ def initialize
3
+ @readings = []
4
+ @mutex = Mutex.new
5
+
6
+ @thread = Thread.new do
7
+ while(true)
8
+ out = {}
9
+ out[:total], out[:free], out[:buffers], out[:cached] = IO.readlines("/proc/meminfo")[0..4].map { |l| l =~ /^.*?\: +(.*?) kB$/; $1.to_i / 1024.0 }
10
+ out[:free_total] = out[:free] + out[:buffers] + out[:cached]
11
+
12
+ @mutex.synchronize do
13
+ @readings.unshift(out)
14
+ @readings.pop while @readings.length > 5
15
+ end
16
+ sleep(2.5)
17
+ end
18
+ end
19
+ end
20
+
21
+ def get
22
+ out = { :total => 0, :free => 0, :buffers => 0, :cached => 0, :free_total => 0 }
23
+ @mutex.synchronize do
24
+ unless @readings.empty?
25
+ out = @readings.first.dup
26
+ out[:status] = 'warning' unless @readings.detect { |r| r[:free] > 5 }
27
+ out[:status] = 'danger' unless @readings.detect { |r| r[:free_total] > 1 }
28
+ end
29
+ end
30
+ out
31
+ end
32
+
33
+ def renderer
34
+ information.merge({ :contents => %{
35
+ sc.innerHTML = "<div class='major_figure'><span class='title'>Free</span><span class='figure'>" + data_source['free'] + "</span><span class='unit'>mb</span></div>" +
36
+ "<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>" +
37
+ "<div class='major_figure'><span class='title'>Total</span><span class='figure'>" + data_source['total'] + "</span><span class='unit'>mb</span></div>";
38
+ } })
39
+ end
40
+
41
+ def information
42
+ { :name => "Memory Info", :in_sentence => 'Memory Usage', :importance => importance }
43
+ end
44
+
45
+ def importance
46
+ 90
47
+ end
48
+
49
+ def kill
50
+ @thread.kill
51
+ end
52
+ end
@@ -0,0 +1,212 @@
1
+ require 'webrick'
2
+
3
+ if $DEBUG
4
+ Thread.abort_on_exception
5
+ else
6
+ exit if fork
7
+ $stdout = File.new('/dev/null', 'w')
8
+ $stderr = File.new('/dev/null', 'w')
9
+ end
10
+
11
+ Thread.new do
12
+ while(true)
13
+ sleep(300)
14
+ GC.start
15
+ end
16
+ end
17
+
18
+ class String
19
+ def underscore
20
+ self.gsub(/::/, '/').
21
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
22
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
23
+ tr("-", "_").
24
+ downcase
25
+ end
26
+ alias_method :to_json, :inspect
27
+ end
28
+
29
+ class Numeric
30
+ def formatted(precision = 1)
31
+ rounded_number = (Float(self) * (10 ** precision)).round.to_f / 10 ** precision
32
+ parts = ("%01.#{precision}f" % rounded_number).to_s.split('.')
33
+ parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
34
+ parts.join(".")
35
+ end
36
+ alias_method :to_json, :inspect
37
+ end
38
+
39
+ class Array
40
+ def to_json
41
+ "[#{map { |e| e.to_json }.join(',')}]"
42
+ end
43
+ end
44
+
45
+ class Hash
46
+ def formatted
47
+ out = self.dup
48
+ out.each_pair { |k, v| out[k] = v.formatted }
49
+ out
50
+ end
51
+ def to_json
52
+ arr = []
53
+ each_pair { |k, v| arr << "#{k.to_json}:#{v.to_json}" }
54
+ "{#{arr.join(',')}}"
55
+ end
56
+ end
57
+
58
+ class Symbol
59
+ def to_json
60
+ to_s.inspect
61
+ end
62
+ end
63
+
64
+ module DataProviders
65
+ DATA_SOURCES = {}
66
+ def self.setup
67
+ Dir.glob("#{File.dirname(__FILE__)}/data_providers/*.rb").each { |file| load file }
68
+ DataProviders.constants.each do |c|
69
+ c = DataProviders.const_get(c)
70
+ DATA_SOURCES[c.to_s.gsub(/^DataProviders::/, '').underscore] = c.new if c.is_a? Class
71
+ end
72
+ end
73
+ end
74
+
75
+ DataProviders.setup
76
+
77
+ class Webstats < WEBrick::HTTPServlet::AbstractServlet
78
+ def do_GET(req, res)
79
+ WEBrick::HTTPAuth.basic_auth(req, res, "Webstats") { |user, pass| user == 'webstats' and pass == ARGV[0] } unless ARGV.empty?
80
+
81
+ body = ""
82
+ if req.path_info == '/'
83
+ body << <<-EOF
84
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
85
+ <html>
86
+ <head>
87
+ <title>Webstats</title>
88
+ <style type="text/css">
89
+ * { margin: 0; padding: 0; font-family: "Lucida Grande", Helvetica, Arial, sans-serif; font-size: 100%; }
90
+ body { font-size: 95%; }
91
+ p { margin: 0 0 1em 0; }
92
+
93
+ h1 { margin: 1em; }
94
+ h1 span { font-size: 160%; font-weight: bold; }
95
+ .source .danger { background-color: #FF0D33; }
96
+ .source .warning { background-color: #F1FF28; }
97
+
98
+ .source { width: 500px; border: 1px solid #000000; margin: 1em; }
99
+ .source h2 { padding: 0 0.8em 0 0.8em; background-color: #C98300; }
100
+ .source h2 span { font-size: 130%; font-weight: bold; padding: 0.2em 0; display: block; }
101
+ .source .source_contents { padding: 0.8em; }
102
+ .source .title { padding-right: 0.5em; }
103
+ .source .major_figure { font-size: 130%; margin: 0.3em 0; }
104
+ .source .major_figure .figure { font-size: 120%; font-weight: bold; font-family: Georgia, serif; }
105
+ .source .major_figure .unit { font-family: Georgia, serif; font-size: 70%; }
106
+ .source .minor_figure { font-family: Georgia, serif; }
107
+ .source .divider { margin-left: 0.2em; margin-right: 0.2em; font-weight: normal; }
108
+ </style>
109
+ <script type="text/javascript">
110
+ var http = null;
111
+
112
+ function getLatest()
113
+ {
114
+ http.open("get", "/update", true);
115
+ http.send(null);
116
+ }
117
+
118
+ window.onload = function()
119
+ {
120
+ http = !!(window.attachEvent && !window.opera) ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
121
+
122
+ http.onreadystatechange = function()
123
+ {
124
+ if(http.readyState == 4)
125
+ {
126
+ var results = eval("(" + http.responseText + ")");
127
+ if(!results) return;
128
+ EOF
129
+
130
+ DataProviders::DATA_SOURCES.each_pair do |k, v|
131
+ 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}
132
+ end
133
+
134
+ body << <<-EOF
135
+ }
136
+ };
137
+
138
+ window.setInterval("getLatest()", 5000);
139
+ getLatest();
140
+ }
141
+ </script>
142
+ </head>
143
+ <body id="body">
144
+ <div id="main">
145
+ <h1><span>Stats for #{req.host}</span></h1>
146
+ EOF
147
+ DataProviders::DATA_SOURCES.sort { |a, b| b[1].importance <=> a[1].importance }.each do |(k, v)|
148
+ r = v.renderer
149
+ body << %{<div class="source" id="source_#{k}"><h2><span>#{r[:name]}</span></h2><div class="source_contents" id="source_contents_#{k}">Loading...</div></div>}
150
+ end
151
+
152
+ body << <<-EOF
153
+ </div>
154
+ </body>
155
+ </html>
156
+ EOF
157
+ elsif req.path_info == '/update'
158
+ out = {}
159
+ DataProviders::DATA_SOURCES.each_pair do |k, v|
160
+ out[k] = v.get
161
+ end
162
+
163
+ fix_leaves_hash(out)
164
+
165
+ body << out.to_json
166
+ elsif req.path_info == '/information'
167
+ out = {}
168
+ DataProviders::DATA_SOURCES.each_pair { |k, v| out[k] = v.information }
169
+ body << out.to_json
170
+ end
171
+
172
+ res.body = body
173
+ res['Content-Type'] = "text/html"
174
+ end
175
+
176
+ private
177
+ def fix_leaves_array(array)
178
+ array.each_with_index do |v, i|
179
+ if v.is_a? Numeric
180
+ array[i] = v.formatted
181
+ elsif v.is_a? Hash
182
+ array[i] = fix_leaves_hash(array[i])
183
+ elsif v.is_a? Array
184
+ array[i] = fix_leaves_array(array[i])
185
+ end
186
+ end
187
+ end
188
+
189
+ def fix_leaves_hash(hash)
190
+ hash.each_pair do |k, v|
191
+ if v.is_a? Numeric
192
+ hash[k] = v.formatted
193
+ elsif v.is_a? Hash
194
+ hash[k] = fix_leaves_hash(hash[k])
195
+ elsif v.is_a? Array
196
+ hash[k] = fix_leaves_array(hash[k])
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ s = WEBrick::HTTPServer.new(:Port => 9970)
203
+
204
+ death = proc do
205
+ s.shutdown
206
+ DataProviders::DATA_SOURCES.each_pair { |k, v| v.kill }
207
+ end
208
+ trap("INT", death)
209
+ trap("TERM", death)
210
+
211
+ s.mount("/", Webstats)
212
+ s.start
data/webstats.gemspec ADDED
@@ -0,0 +1,52 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{webstats}
5
+ s.version = "0.1.0"
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-04-22}
10
+ s.default_executable = %q{webstats}
11
+ s.description = %q{Display server CPU/Memory/Disk Usage on a web page, suitable for remote performance monitoring.}
12
+ s.email = %q{i@bloople.net}
13
+ s.executables = ["webstats"]
14
+ s.extensions = ["server/data_providers/extconf.rb"]
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README"
18
+ ]
19
+ s.files = [
20
+ "LICENSE",
21
+ "README",
22
+ "Rakefile",
23
+ "VERSION.yml",
24
+ "bin/webstats",
25
+ "clients/email_notifier/email_notifier.rb",
26
+ "clients/growl_notifier/Growl.rb",
27
+ "clients/growl_notifier/growl_notifier.rb",
28
+ "server/data_providers/cpu_info.rb",
29
+ "server/data_providers/disk_activity.rb",
30
+ "server/data_providers/disk_usage.c",
31
+ "server/data_providers/disk_usage.rb",
32
+ "server/data_providers/extconf.rb",
33
+ "server/data_providers/mem_info.rb",
34
+ "server/webstats.rb",
35
+ "webstats.gemspec"
36
+ ]
37
+ s.homepage = %q{http://github.com/bloopletech/webstats}
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.require_paths = [""]
40
+ s.rubygems_version = %q{1.3.1}
41
+ s.summary = %q{Display server CPU/Memory/Disk Usage on a web page, suitable for remote performance monitoring.}
42
+
43
+ if s.respond_to? :specification_version then
44
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
45
+ s.specification_version = 2
46
+
47
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
48
+ else
49
+ end
50
+ else
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bloopletech-webstats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Brenton Fletcher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-04-22 00:00:00 -07:00
13
+ default_executable: webstats
14
+ dependencies: []
15
+
16
+ description: Display server CPU/Memory/Disk Usage on a web page, suitable for remote performance monitoring.
17
+ email: i@bloople.net
18
+ executables:
19
+ - webstats
20
+ extensions:
21
+ - server/data_providers/extconf.rb
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README
25
+ files:
26
+ - LICENSE
27
+ - README
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - bin/webstats
31
+ - clients/email_notifier/email_notifier.rb
32
+ - clients/growl_notifier/Growl.rb
33
+ - clients/growl_notifier/growl_notifier.rb
34
+ - server/data_providers/cpu_info.rb
35
+ - server/data_providers/disk_activity.rb
36
+ - server/data_providers/disk_usage.c
37
+ - server/data_providers/disk_usage.rb
38
+ - server/data_providers/extconf.rb
39
+ - server/data_providers/mem_info.rb
40
+ - server/webstats.rb
41
+ - webstats.gemspec
42
+ has_rdoc: false
43
+ homepage: http://github.com/bloopletech/webstats
44
+ post_install_message:
45
+ rdoc_options:
46
+ - --charset=UTF-8
47
+ require_paths:
48
+ - ""
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.2.0
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: Display server CPU/Memory/Disk Usage on a web page, suitable for remote performance monitoring.
68
+ test_files: []
69
+