package org.curriki.tools.monitor;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import java.io.*;
import java.text.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** process to construct a set of HTML pages from the stand
*/
public class MonitorWebRenderer implements MonitoringConstants {
private File baseDir = new File("output");
private long start, end;
private String pidAsString;
private float maxCpuLoad = 0;
private static final float intervalBetweenTops = 5000;
private static DateFormat apacheLogDf = new SimpleDateFormat(APACHE_LOG_DATEFORMAT),
appservLogDf = new SimpleDateFormat(APPSERV_LOG_DATEFORMAT),
directoryNameDf = new SimpleDateFormat(DIRNAME_DATEFORMAT);
// example 2011-11-28T13:13:11.901-0800
private static NumberFormat decimals = new DecimalFormat("######.##"),
integers = new DecimalFormat("#####");
public static void main(String[] args) throws Exception {
long end = System.currentTimeMillis();
long start = end - 60*1000;
if(args.length>1 && args[1].startsWith("[")) {
start = apacheLogDf.parse(args[1].substring(1, args[1].length()-1)).getTime();
end = start+60*1000;
}
process(start, end, args[0]);
}
public static void process(long start, long end, String pid) throws Exception {
MonitorWebRenderer mwr = new MonitorWebRenderer(start, end, pid);
try {
mwr.topParser.parse();
} catch (Exception e) { e.printStackTrace(); }
try {
mwr.apacheLogParser.parse();
} catch (Exception e) { e.printStackTrace(); }
try {
mwr.appservLogParser.parse();
} catch (Exception e) { e.printStackTrace(); }
try {
mwr.mySQLProcessListSplitter.parse();
} catch (Exception e) { e.printStackTrace(); }
// copy all files
StringBuilder sourcesLinks = new StringBuilder("");
for(File file: new File(System.getProperty("user.dir")).listFiles()) {
if(!file.isFile() ||
file.getName().endsWith(".sh") ||
"index.html".equals(file.getName()) ||
"log".equals(file.getName()) ) continue;
String name = file.getName();
FileUtils.copyFile(file, new File(mwr.baseDir, name));
sourcesLinks.append("<a href='").append(name).append("'>").append(name).append("</a> - ");
}
// create home page
Map<String,String> values = new HashMap<String,String>();
values.put("startDate", apacheLogDf.format(new Date(start)));
values.put("endDate", apacheLogDf.format(new Date(end)));
values.put("cacheEvictions", FileUtils.readFileToString(new File("cacheEvictions.js")));
values.put("maxCpuLoad", integers.format(Math.round(mwr.maxCpuLoad / 100) * 100 + 100));
values.put("sourcesLinks", sourcesLinks.toString());
values.put("pageLoadMeasures", mwr.pageLoadMeasuresSb.toString());
values.put("nextURL", directoryNameDf.format(new Date(start+5L*60*1000)));
values.put("prevURL", directoryNameDf.format(new Date(start-5L*60*1000)));
Class clz = MonitorWebRenderer.class;
Util.substituteStream(
clz.getResourceAsStream("monitorResources/monitor-curriki-template.html"),
new FileOutputStream("output/index.html"), values);
// finally copy all included files
new File("output/js").mkdirs();
for(String fileName: Arrays.asList("excanvas.min.js", "jquery.jqplot.min.css",
"jquery.jqplot.min.js", "jquery.min.js", "logRoller.js")) {
FileUtils.copyURLToFile(clz.getResource("monitorResources/js/" + fileName),
new File("output/js/" + fileName));
}
// rename output to a directory with proper date
new File("output").renameTo(new File(directoryNameDf.format(start)));
}
public MonitorWebRenderer(long start, long end, String pidAsString){
this.start = start;
this.end = end;
this.pidAsString = pidAsString;
baseDir.mkdirs();
topParser = new TopParser();
apacheLogParser = new ApacheLogParser();
appservLogParser = new AppservLogParser();
mySQLProcessListSplitter = new MySQLProcessListSplitter();
pageLoadMeasuresSb = new StringBuilder();
pageLoadParsers = new PageLoadParser[URLS_TO_MONITOR.length];
for(int i=0; i<URLS_TO_MONITOR.length; i++) {
pageLoadParsers[i] = new PageLoadParser(MonitorPageLoadTime.urlToFile(URLS_TO_MONITOR[i]), start, end, (int) ((end-start)/intervalBetweenTops));
pageLoadParsers[i].run();
pageLoadMeasuresSb.append(pageLoadParsers[i].toJSArrayDecl("pageLoad[" + i + "]"));
pageLoadMeasuresSb.append("pageLoad[").append(i).append("].url='").append(URLS_TO_MONITOR[i]).append("';\n" );
pageLoadMeasuresSb.append("\n");
}
}
private TopParser topParser;
private ApacheLogParser apacheLogParser;
private AppservLogParser appservLogParser;
private MySQLProcessListSplitter mySQLProcessListSplitter;
private PageLoadParser[] pageLoadParsers;
private StringBuilder pageLoadMeasuresSb;
private static class Util {
private static void substituteStream(InputStream in, OutputStream out, Map<String, String> values) throws IOException {
String[] pieces = IOUtils.toString(in, "utf-8").split("\\^");
Writer outW = new OutputStreamWriter(out, "utf-8");
for(String piece: pieces) {
String v = values.get(piece);
if(v==null) v = piece;
outW.write(v);
}
outW.flush(); outW.close();
in.close();
}
static String readTillStartsWith(LineNumberReader reader, String prefix, PrintWriter putLinesIn) throws IOException {
String read;
while((read = reader.readLine())!=null) {
if(putLinesIn!=null) putLinesIn.println(read);
if(read.startsWith(prefix)) return read;
}
return null;
}
static void outputJsonPairsForJQPlot(PrintWriter out, String varName, float[] vals, boolean removeZeroes) {
out.print(varName); out.print("= [");
int numBuckets = vals.length;
for(int i=0; i<numBuckets; i++) {
if(vals[i]==0 && removeZeroes) continue;
out.print("[");
out.print(integers.format((int) (i*intervalBetweenTops/1000)));
out.print(", ");
out.print(decimals.format(vals[i]));
out.print("]");
if(i+1<numBuckets) out.print(",");
}
out.println("];");
}
static void outputJsonHash(PrintWriter out, String varName, int[] vals) {
out.print(varName); out.print(" = {");
int numBuckets = vals.length;
for(int i=0; i<numBuckets; i++) {
out.print("'");
out.print(integers.format((int) (i*intervalBetweenTops/1000)));
out.print("':'");
out.print(integers.format(vals[i]));
out.print("'");
if(i+1<numBuckets) out.print(",");
}
out.println("};");
}
public static LineNumberReader readFile(String fileName) throws IOException {
return new LineNumberReader( new InputStreamReader(new FileInputStream(fileName),"utf-8"));
}
public static PrintWriter makePrintWriter(String fileName) throws IOException {
System.err.println("--- outputting " + fileName);
return new PrintWriter(new OutputStreamWriter(new FileOutputStream(fileName), "utf-8"));
}
}
private class TopParser {
float[] cpuLoads;
int numBuckets;
private TopParser() {
numBuckets = (int) ((end-start)/intervalBetweenTops);
cpuLoads = new float[numBuckets];
}
void parse() throws Exception {
LineNumberReader in = Util.readFile("tops.txt");
int count=0; String read = "";
while(count< numBuckets) {
read = Util.readTillStartsWith(in, "load averages", null);
// first read the header
PrintWriter out = Util.makePrintWriter("output/topsHeader_"+count+".txt");
while((read=in.readLine())!=null && read.trim().length()>0) {
out.println(read);
}
// then read the first line (column headers
read = in.readLine();
out.println(read);
// then find the line with the right pid and put it there
while((read=in.readLine())!=null && read.trim().length()>0) {
if(read.trim().startsWith(pidAsString)) {
out.println(read);
collectCpuLoad(read, count);
}
}
out.flush(); out.close();
count++;
}
// now output CPU load as a json;
PrintWriter out = Util.makePrintWriter("output/topsCpuLoad.js");
Util.outputJsonPairsForJQPlot(out, "cpuLoad", cpuLoads, false);
out.flush(); out.close();
}
private void collectCpuLoad(String topLine, int count) throws Exception {
String[] cols = topLine.split(" |\t");
String cpuLoad = null;
for(String col: cols) {
if(col.endsWith("%"))
cpuLoad = col;
}
if(cpuLoad==null) return;
cpuLoad = cpuLoad.substring(0, cpuLoad.length()-1);
float load = decimals.parse(cpuLoad).floatValue();
if(load > maxCpuLoad) maxCpuLoad = load;
cpuLoads[count] = load;
}
}
private class ApacheLogParser {
TimeToLine ttl = new TimeToLine(intervalBetweenTops, start, end, "ApacheLog");
void parse() throws Exception {
PrintWriter out = Util.makePrintWriter("output/apacheLogs.html");
String[] pageTemplateBits =
IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/apacheLogsTemplate.html"), "utf-8")
.split("\\^");
if(pageTemplateBits.length != 7 || ! ("log".equals(pageTemplateBits[1]) && "date".equals(pageTemplateBits[3]) && "data".equals(pageTemplateBits[5]) )) {
System.err.println("template was: " + IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/apacheLogsTemplate.html"), "utf-8"));
System.err.println("which split to: ");
for(String bit : pageTemplateBits) System.err.println("- " + bit);
throw new IllegalStateException("Template is malformed, expected sequence log, date, data.");
}
out.println(pageTemplateBits[0]);
// put logs in
LineNumberReader in = Util.readFile("apacheLog.txt");
String line;
Pattern pattern = Pattern.compile("[^\\[]*\\[([^]]+)\\].*");
int lineNum = 0;
while( (line = in.readLine())!=null ) {
if(line.trim().startsWith("|") || line.startsWith("Killed by signal") || line.endsWith(" Terminated") || line.startsWith("filename is "))
continue;
Date d = null;
try {
Matcher m = pattern.matcher(line);
if(m.matches()) {
d = apacheLogDf.parse(m.group(1));
ttl.add(d, lineNum);
} else
throw new IllegalStateException("Can't find date in line \"" + line + "\".");
} catch (Exception e) {
System.err.println("Date mismatch in \"" + line + "\".");
e.printStackTrace();
}
out.print("<span id='");
out.print(integers.format(lineNum));
if(d!=null) {
out.print("' time='");
out.print(integers.format((int) (ttl.convertToTimeDiff(d)/1000)));
//out.print(integers.format(ttl.convert(d)));
}
out.print("'>");
out.print(line);
out.println("</span>");
lineNum++;
}
// footer
out.print(pageTemplateBits[2]);
out.print("Created on ");
out.println(new Date());
out.print(pageTemplateBits[4]);
Util.outputJsonHash(out, "Curriki.monitor.timeToAnchor", ttl.buckets);
out.print(pageTemplateBits[6]);
out.flush();
out.close();
in.close();
}
}
// =======================================================================================================
private class AppservLogParser {
TimeToLine ttl = new TimeToLine(intervalBetweenTops, start, end, "AppservLog");
int threadDumpNum = 0;
void parse() throws Exception {
PrintWriter out = Util.makePrintWriter("output/appservLogs.html");
String[] pageTemplateBits =
IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/appservLogsTemplate.html"), "utf-8")
.split("\\^");
if(! ("log".equals(pageTemplateBits[1]) && "date".equals(pageTemplateBits[3]) && "data".equals(pageTemplateBits[5]) )) {
System.err.println("template was: " + IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/apacheLogsTemplate.html"), "utf-8"));
System.err.println("which split to: ");
for(String bit : pageTemplateBits) System.err.println("- " + bit);
throw new IllegalStateException("Template is malformed, expected sequence log, date, data.");
}
out.println(pageTemplateBits[0]);
// put logs in
LineNumberReader in = Util.readFile("appservLogs.txt");
String line;
Pattern pattern = Pattern.compile("[^|]*\\|([^|]+)\\|.*");
int lineNum = 0;
Date date = null;
while( (line = in.readLine())!=null ) {
// a thread-dump? put it aside
if(line.startsWith("Full thread dump")) {
PrintWriter pw = Util.makePrintWriter("output/threads_" + threadDumpNum + ".html");
pw.println("<html><head><title>Threads</title>" +
" <script type='text/javascript' src='js/jquery.min.js'></script>\n" +
" <script type='text/javascript' src='js/logRoller.js'></script>\n" +
"</head><body><pre>");
pw.println(line);
pw.println("</pre>\n<ul><li onclick='Curriki.monitor.foldUnfold(this);'><pre>");
int count = 0;
String d = "";
while((line = in.readLine())!=null && !line.startsWith(" concurrent-mark-sweep perm gen total")) {
if(line.length()==0 && count<2) continue;
if(count==3) {
pw.print("</pre><div style='display:none'><pre>");
d="</div>";
}
pw.println(line);
count++;
if(line.trim().length()==0) {
pw.write("</pre>");
pw.write(d);
pw.write("</li>\n<li onclick='Curriki.monitor.foldUnfold(this);'><pre>");
count = 0;
d="";
}
}
pw.println(line);
pw.println("</pre>"+d+"</li></ul>");
pw.println("</body></html>");
pw.flush(); pw.close();
threadDumpNum++;
}
if(line==null) break;
// ignored lines
if(line.trim().startsWith("|") || line.startsWith("Killed by signal")
|| line.trim().length()==0
|| "[ERROR] |#]".equals(line) || "[WARNING] |#]".equals(line))
continue;
// lines without dates
if(! (line.startsWith("[GC ["))) try {
Matcher m = pattern.matcher(line);
if(m.matches()) {
date = appservLogDf.parse(m.group(1));
ttl.add(date, lineNum);
}
} catch (Exception e) {
System.err.println("Date mismatch in \"" + line + "\".");
e.printStackTrace();
}
out.print("<span id='");
out.print(integers.format(lineNum));
if(date!=null) {
out.print(" time='");
String s = integers.format((int) (ttl.convertToTimeDiff(date)/1000));
out.print(s);
}
out.print("'>");
out.print(line);
out.println("</span>");
lineNum++;
}
// footer
out.print(pageTemplateBits[2]);
out.print("Created on ");
out.println(new Date());
out.print(pageTemplateBits[4]);
Util.outputJsonHash(out, "Curriki.monitor.timeToAnchor", ttl.buckets);
out.print(pageTemplateBits[6]);
out.flush();
out.close();
in.close();
}
}
// ==============================================================================
private class MySQLProcessListSplitter {
PrintWriter out = null;
int num = 0;
String[] pageTemplateBits;
private void renewOut() throws IOException {
if(out!=null) {
out.print(pageTemplateBits[2]);
out.print(new Date());
out.print(pageTemplateBits[4]);
out.flush(); out.close();
}
String fileName = "output/mysqlProcesses_"+num+".html";
out = Util.makePrintWriter(fileName);
out.println(pageTemplateBits[0]);
}
private void parse() throws Exception {
pageTemplateBits = IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/mysqlProcesses_x_Template.html"), "utf-8")
.split("\\^");
if(! (pageTemplateBits.length==5 && "log".equals(pageTemplateBits[1]) && "date".equals(pageTemplateBits[3]) )) {
System.err.println("Faulty template: " + IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/mysqlProcesses_x_Template.html"), "utf-8"));
System.err.println("template was: " + IOUtils.toString(this.getClass().getResourceAsStream("monitorResources/apacheLogsTemplate.html"), "utf-8"));
System.err.println("which split to: ");
int i=0;
for(String bit : pageTemplateBits) System.err.println("- " + (i++) + " " + bit);
throw new IllegalStateException("Template is malformed, expected sequence log, date.");
}
LineNumberReader in = Util.readFile("mysqlProcessList.txt");
String line;
renewOut();
while( (line = in.readLine())!=null ) {
if("----------------------".equals(line)) {
num++;
renewOut();
continue;
}
out.println(line);
}
out.flush();
out.close();
in.close();
}
}
// =======================================================================================================
private class TimeToLine {
private TimeToLine(float granularity, long start, long end, String name) {
this.start = start;
this.end = end;
this.name = name;
this.granularity = granularity;
this.numBuckets = (int) ((end-start)/granularity + 0.5f);
this.buckets = new int[numBuckets];
for(int i=0; i<numBuckets; i++) buckets[i] = -1;
}
float granularity;
long start, end;
String name;
int numBuckets;
int[] buckets;
void add(Date date, int lineNum) {
int bucketNum = convert(date);
if(bucketNum<0 || bucketNum>=buckets.length) {
System.err.println("Ignoring date in " + name + " \"" + date + "\" being out of range (" + bucketNum + " of " + numBuckets + ").");
} else {
int existing = buckets[bucketNum];
if(existing==-1 || existing > lineNum)
buckets[bucketNum] = lineNum;
}
}
public int convert(Date date) {
return (int) (convertToTimeDiff(date)/granularity);
}
public int convertToTimeDiff(Date date) {
int r= (int) ((date.getTime()-start));
return r;
}
}
// ==============================================================================
// reads lines of the form
// [13/12/2011:20:57:10 +0100] 0.89 s http://www.curriki.org/ : HTTP/1.1 200 OK
// and converts them to an array of measures
static class PageLoadParser {
public PageLoadParser(String fileName, long startTime, long endTime, int numSteps) {
this.fileName = fileName;
this.startTime = startTime;
this.endTime = endTime;
this.numSteps = numSteps;
this.interval = (endTime-startTime)/numSteps;
this.times = new float[numSteps];
}
private float[] times;
private String fileName;
private long startTime, endTime, interval;
private int numSteps = 0;
private DateFormat df = new SimpleDateFormat(PAGE_LOAD_DATEFORMAT_PATTERN);
public void run() {
try {
LineNumberReader in = Util.readFile(fileName);
String line;
Pattern pattern = Pattern.compile("\\[([^\\]]+)\\] ([0-9\\.]+) s .*");
while((line=in.readLine())!=null) {
Matcher matcher = pattern.matcher(line);
if(!matcher.matches()) {
System.err.println("Line non matching! ");
System.err.println(" - " + line);
} else {
long time = df.parse(matcher.group(1)).getTime();
float duration = Float.parseFloat(matcher.group(2));
if(time<startTime || time>=endTime) {
System.err.println("No good date in PageLoad " + matcher.group(1) );
continue;
}
time = time-startTime;
int pos = (int) (time/interval);
times[pos] = duration;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void expressArray(String varName, PrintWriter out) {
Util.outputJsonPairsForJQPlot(out, varName, times, true);
}
public String toJSArrayDecl(String varName) {
StringWriter out = new StringWriter();
PrintWriter pout = new PrintWriter(out);
expressArray(varName, pout);
pout.flush();
return out.toString();
}
}
}