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/"
Options Indexes FollowSymLinks MultiViews
AllowOverride all
Order allow,deny
Allow from all
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.

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.

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.

Hope you find that hack useful.

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
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
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
nice pickup sameh! =)