Roll Your Own SiteScope, a Simple Alternative

23 April, 2008 – 6:06 pm

In working with SiteScope of late, I’ve found that it doesn’t always collect performance metrics the way I want to. More importantly, it can often turn a simple monitoring activity into a complex disaster. Take monitoring via JMX for example. In SiteScope, it has a rather complicated (and sometimes broken) interface when trying to communicate with a busy MBean Server. One can quite easily roll your own JMX monitor using open source tools in about 65 lines of code as I demonstrated here.

But we still all use tools like LoadRunner in these commercial 9-5 contracts right? Wouldn’t it be nice, if you could roll your own custom monitors in Ruby, Perl or whatever language you like, store that data in a simple repository, let’s say a MySQL database, and still be able to hook into those metrics from a LoadRunner Controller during test execution!?

It is possible, with one PHP file and a simple WAMP (or LAMP) installation all wrapped up in a SiteScope-like alternative.

First thing, is you will probably want to get data from a variety of sources/platforms/operating systems. Using common scripting languages it is possible to roll your own remote agents.

For example, the following UDP server written in Perl, will listen on port 5151 in a central location, possibly on the same box as your SiteScope alternative.
I’ve also added some custom formatting templates for the type of remote monitors you might engage (vmstat, sar, iostat etc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/perl -w
use IO::Socket;
 
my($sock, $oldmsg, $msg, $hisaddr, $hishost, $MAXLEN, $PORTNO);
$MYSQL  = "D:\\wamp\\bin\\mysql\\mysql5.0.51a\\bin\\mysql -u root -D perf -e ";
$MAXLEN = 1024;
$PORTNO = 5151;
$sock = IO::Socket::INET->new(LocalPort => $PORTNO, Proto => 'udp')
    or die "socket: $@";
 
print "Awaiting UDP messages on port $PORTNO\n";
while ($sock->recv($msg, $MAXLEN)) {
    my($port, $ipaddr) = sockaddr_in($sock->peername);
    $hishost    = gethostbyaddr($ipaddr, AF_INET);
    $table      = ($msg =~ /^([\w\_]+):/g) ? $1 : undef;
    $msg        =~ s/^\w+://g;
 
	# Print unformatted message
    print "$table, $hishost,$msg\n";
 
    # Apply formatting rules
    if($table   =~ /vmstat/g){
		# vmstat 60 120 | tee vmstat.$HOSTNAME.csv | ./udpClient.pl monitorHost perf_vmstat
        $msg    =~ s/^\s+//g;
        $msg    =~ s/\s+/,/g;
        my $now = &formatTime();
        my $cmd =  $MYSQL."\"INSERT INTO $table VALUES('','$hishost',$now ,$msg)\"";
		my $ret = `$cmd` if($msg =~ /^\d+/g);
    }
 
	if($table   =~ /iostat/g){
		# iostat -xnMrTuc 60 120 | tee iostat.$HOSTNAME.csv | ./udpClient.pl monitorHost  perf_iostat
		$msg    =~ s/([\d\w]+$)/'$1'/g;
        my $now = &formatTime();
        my $cmd =  $MYSQL."\"INSERT INTO $table VALUES('','$hishost',$now ,$msg)\"";
		my $ret = `$cmd` if($msg =~ /^\d+\.\d+/g);
    }
 
	if($table   =~ /sar/g){
		# sar 60 120 | tee sar.$HOSTNAME.csv | ./udpClient.pl monitorHost perf_sar
		$msg    =~ s/(^\d+:\d+:\d+)//g;
		$msg    =~ s/^\s+//g;
		$msg    =~ s/\s+/,/g;
        my $now = &formatTime();
        my $cmd =  $MYSQL."\"INSERT INTO $table VALUES('','$hishost',$now ,$msg)\"";
		my $ret = `$cmd` if($msg =~ /^\d+/g);
    }
 
    $sock->send("ACK");
} 
die "recv: $!";
 
sub formatTime
{
	use POSIX qw( strftime );
	my $year	= strftime "%Y", localtime;
	my $month	= strftime "%m", localtime;
	my $day	    = strftime "%d", localtime;
	my $hour	= strftime "%H", localtime;
	my $min	    = strftime "%M", localtime;
	my $sec	    = strftime "%S", localtime;
	my $now     ="'$year-$month-$day', '$hour:$min:$sec'";
	return $now;
}

Given that many Operating Systems are likely to have Perl already installed, a remote UDP client written in Perl will suffice. This significantly lessens the footprint of your remote agents.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/perl -w
use IO::Socket;
my($sock, $server_host, $msg, $port, $ipaddr, $hishost, 
       $MAXLEN, $PORTNO, $TIMEOUT, $line);
 
$server_host = $ARGV[0] || "monitorHost";
$table 		 = $ARGV[1] || "undef";
while (defined($line = <STDIN>)) {
 
    chomp $line;
    if($line =~ /[\w\d]+/) {
 
    $MAXLEN  = 1024;
    $PORTNO  = 5151;
    $TIMEOUT = 10;
 
    $msg         = "$table:$line";
    $sock = IO::Socket::INET->new(Proto     => 'udp',
                                  PeerPort  => $PORTNO,
                                  PeerAddr  => $server_host)
        or die "Creating socket: $!\n";
    $sock->send($msg) or die "send: $!";
 
    eval {
        local $SIG{ALRM} = sub { die "alarm time out" };
        alarm $TIMEOUT;
        $sock->recv($msg, $MAXLEN)      or die "recv: $!";
        alarm 0;
        1;  # return value from eval on normalcy
    } or die "recv from $server_host timed out after $TIMEOUT seconds.\n";
    }    
}

So what we’ve just achieved there, is a simple UDP client/server, whereby you can pipe in any type of command on the monitored host into your UDP client. Eg:
sar 60 120 | tee sar.$HOSTNAME.csv | ./udpClient.pl monitorHost perf_sar

That code will instruct the remote agent to pipe the results of sar into a csv (just in case) and into the udpClient. The extra parameter arguments just tell the script which host has the udpServer running and also which database table the results should be fed into.

Meanwhile, back at the ranch, the udpServer is collecting these results over a UDP connection (nice and light), doing some formatting (you can be creative here) and basically inserting that data straight into a MySQL database. I haven’t bothered with a DBI interface (as I’m trying to work with a basic Perl installation), so am just using mysql -e with insert statements from a command line.

This is just one way to get data in. I often have other scripts running such as Ruby, perl, typeperf (perfmon) and so on running in the background whilst feeding data into the database.

So now we have a populated database… what now?

If you’re using LoadRunner Controller, you might be interested in getting that information out dynamically during test execution, rather than relying on bulk inserts of data in Analysis. Using my same WAMP box, I wrote a SiteScope alternative or ‘faker’, which reads from the database and presents the relevant XML as SiteScope normally would.

You need to create an alias for SiteScope to wherever you’ve got the code hosted. Eg:
Alias /SiteScope/ "D:/Sites/SiteScopeFaker/"
 
<Directory "D:/Sites/SiteScopeFaker/">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride all
        Order allow,deny
    Allow from all
</Directory>

Then make sure you’ve got the sub directory path that SiteScope would normally present to the LoadRunner Controller. Eg:
D:\Sites\SiteScopeFaker\cgi\go.exe\

Also make sure you edit your .htaccess for your alias to serve up everything as php, even if the extension is missing. Eg:
ForceType application/x-httpd-php

And finally, add the php code below to a file called ‘SiteScope’ to the go.exe\ subdirectory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php
$catalog = 'mydb';
 
# Not overly concerned about security here, lock it down as you must ...
if(!$dbconnect = mysql_connect('localhost:3306', 'root', '')) {
   echo "Connection failed to the host 'localhost'.";
   exit;
} 
if (!mysql_select_db($catalog)) {
   echo "Cannot connect to database $catalog";
   exit;
} 
 
# create root element
$doc = new DomDocument('1.0');
$root = $doc->createElement('PerformanceMonitor');
$root = $doc->appendChild($root);
 
# get tables to process
# ---------------------
$dbtables = array();
if (isset($_GET["table"])) {
    $dbtables[] = $_GET["table"];
} else {
    $query  = "show tables";
    $tables = mysql_query($query, $dbconnect);
    while($table = mysql_fetch_assoc($tables)) {
        $dbtables[] = $table["Tables_in_$catalog"];
    }
}
 
# get date time brackets if available
# -----------------------------------
if (isset($_GET["df"])) $df = $_GET["df"];
if (isset($_GET["dt"])) $dt = $_GET["dt"];
if (isset($_GET["tf"])) $tf = $_GET["tf"];
if (isset($_GET["tt"])) $tt = $_GET["tt"];
 
 
# process each table
# ------------------
foreach($dbtables as $table){
        if(strpos($table, "perf_")>-1){	
            $table_element = $doc->createElement("object");
            $table_element->setAttribute("class", "group");
            $table_element->setAttribute("name", $table);
            $table_element->setAttribute("desc", $table);
            $table_element = $root->appendChild($table_element);
 
            # set up counters
            # ---------------
            $counters = array();
            $query   = "select * from $table limit 1";
            $fields = mysql_query($query, $dbconnect);
            $fields = mysql_fetch_assoc($fields);
            $has_groupid 	    = false;        
            $has_subgroupid 	= false;
            foreach ($fields as $field => $value) {
                $exclusions = '/rowid|^date|^time|groupid|subgroupid/';  
                if (!preg_match($exclusions, $field)) {
                    $counters[] = $field;
                }
                if($field == "groupid") 	$has_groupid 	= true;
                if($field == "subgroupid")  $has_subgroupid = true;
            }
 
            # set up filters by groups or subgroups
            # -------------------------------------
            $filters = array();        
            if($has_subgroupid) {
                $query       = "select groupid, subgroupid from $table group by groupid, subgroupid";
                $groups      = mysql_query($query, $dbconnect);
                $num_groups  = mysql_num_rows($groups);
                while($group = mysql_fetch_assoc($groups)) $filters[]       = $group[groupid]."_".$group[subgroupid];
            } else {
                $query       = "select groupid from $table group by groupid";
                $groups      = mysql_query($query, $dbconnect);
                $num_groups  = mysql_num_rows($groups);
                while($group = mysql_fetch_assoc($groups)) $filters[]       = $group[groupid];
            }
 
            # now get data
            # ------------
            $query   = "select * from $table order by rowid desc limit $num_groups";
            $results = mysql_query($query, $dbconnect);
            $num_results = mysql_num_rows($results);
 
            if ($num_results > 0) {
                foreach ($counters as $counter) {
                    $allowed = "/[^a-z0-9\\.\\-\\_\\\\]/i";
                    $counter_name = preg_replace($allowed,"", $counter);
                    $counter_element = $doc->createElement("object");
                    $counter_element->setAttribute("class", "monitor");
                    $counter_element->setAttribute("name", $counter_name);
                    $counter_element->setAttribute("desc", $counter_name);
                    $counter_element->setAttribute("type", "custom");
                    $counter_element = $table_element->appendChild($counter_element);
 
                    foreach ($filters as $filter){
                        $data  = array();
                        while($result = mysql_fetch_assoc($results)) {
                            if($filter == $result[groupid]."_".$result[subgroupid]) $data[] = $result[$counter]; 
                            else if($filter == $result[groupid]) $data[] = $result[$counter]; 
                        }
                        mysql_data_seek ($results, 0);
 
                        #array_pop($data)
                        $filter = preg_replace($allowed,"", $filter);
                        $value_element = $doc->createElement("counter");
                        $value_element->setAttribute("name", $filter);
                        $value_element->setAttribute("desc", $filter);
                        if($operation != "config") $value_element->setAttribute("val", array_pop($data));
                        $value_element = $counter_element->appendChild($value_element);
                    }
                }
            } else {
                echo "No data for returned for query:<br/> $query";
            }
        }
}
 
// get completed xml document
$xml_string = $doc->saveXML();
echo $xml_string;
?>

What you end up with is a nicely presented SiteScope alternative within LoadRunner.
SiteScope Faker

As long as your database tables start with the prefix ‘perf_’. Eg.:
perf_sar

The SiteScope faker when running will read all the columns from that table and wrap it up as available groups, monitors and counters within a normal ‘Add Measurements’ for SiteScope dialog.
SiteScope Faker Counter Dialog

And finally, when creating your perf_tables, you just need to make sure they follow a certain layout, in that you must have a rowid, date, time, group and optional [subgroup] columns so that the SiteScope faker can correctly organise your counters.
SiteScope Faker Columns

Hope you find that hack useful.
:)

Share it: These icons link to social bookmarking sites where readers can share and discover new web pages.
  • Digg
  • del.icio.us
  • Netscape
  • Reddit
  • Slashdot
  • Technorati
  • YahooMyWeb
  1. 4 Responses to “Roll Your Own SiteScope, a Simple Alternative”

  2. FYI, to specify the port number when adding the monitor …

    To change the default port that LoadRunner connects to for Sitescope, you can add the port to the end of the server name when adding the monitor.
    Example:
:80

    Alternatively, change the default port in LoadRunner Configuration file 
1. Navigate to /dat/monitors
2. Open xmlmonitorshared.ini in notepad. 
3. Under the [SiteScope] section ,change the following setting
    DefaultPort= 
   PortNumber is the port where sitescope server is running

    By Tim on Sep 25, 2008

  3. In the PHP above - what value should groupid have - is it supposed to be a constant?

    I am getting the following when I try to run it:

    Notice: Use of undefined constant groupid

    By Nick on Dec 10, 2008

  4. Alternatively, instead of building your own directory tree “cgi/go.exe” etc, you could modify this file:

    C:\Program Files\HP\LoadRunner\dat\monitors\xmlmonitorshared.ini

    There you will find yourself with some code under the SiteScope section. You may modify port and url information, so you don’t have to change the port your apache server is running on, if you are hosting other applications already:

    [SiteScope]
    ;ExtensionDll=SiteScopeMonExt.dll
    MetricDataURL=SiteScope/cgi/go.exe/SiteScope?page=topaz
    MetricListURL=SiteScope/cgi/go.exe/SiteScope?page=topaz&operation=config
    DefaultPort=8888
    DlgTitle=SiteScope Monitor
    RefreshMetricList=1
    EnableAccount=1

    Keep in mind, you will need to modify this on each controller, however, you could write a shell script to replace the contents of this file once you visit the site, or automate something of the sort….

    Good work koops :)

    By Sameh on May 7, 2009

  5. nice pickup sameh! =)

    By Tim on May 8, 2009

Post a Comment

*
To prove that you're not a bot, enter this code
Anti-Spam Image