package com.ibm.nmon.parser; import org.slf4j.Logger; import java.util.List; import java.util.Map; import java.io.IOException; import java.io.File; import java.io.LineNumberReader; import java.util.regex.Pattern; import java.text.SimpleDateFormat; import java.text.ParseException; import com.ibm.nmon.data.BasicDataSet; import com.ibm.nmon.data.DataRecord; import com.ibm.nmon.data.DataType; import com.ibm.nmon.data.SubDataType; import com.ibm.nmon.util.DataHelper; /** * Parser for zpool's iostat command. Will parse the data from <code>zpool iostat SAN_ZPOOL -vTd</code>. */ public final class ZPoolIOStatParser { private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(ZPoolIOStatParser.class); private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("EEE MMM dd HH:mm:ss z yyyy"); private static final Pattern DATA_SPLITTER = Pattern.compile("\\s+"); private static final int EXPECTED_DATA_TYPES = 6; public static final String DEFAULT_HOSTNAME = "zpool"; private LineNumberReader in = null; private BasicDataSet data = null; public BasicDataSet parse(File file) throws IOException, ParseException { return parse(file.getAbsolutePath()); } public BasicDataSet parse(String filename) throws IOException, ParseException { long start = System.nanoTime(); data = new BasicDataSet(filename); data.setHostname(DEFAULT_HOSTNAME); // DataType type = new DataType(id, name, fields) // create a sub data type for each pool, mirror, disk, etc List<DataType> types = new java.util.ArrayList<DataType>(EXPECTED_DATA_TYPES); // track all disks seen and assign unique names List<String> diskNames = new java.util.ArrayList<String>(8); Map<String, Integer> nameCounts = new java.util.HashMap<String, Integer>(8); String line = null; try { in = new LineNumberReader(new java.io.FileReader(filename)); while ((line = in.readLine()) != null) { long time = TIMESTAMP_FORMAT.parse(line).getTime(); DataRecord record = new DataRecord(time, line); // header lines discardLine(); discardLine(); // ---- separator line discardLine(); // used to hold all the values for a data record so they can be transposed from disk (row) / metric // (column) to metric (data type) / disk (field) List<List<Double>> values = new java.util.ArrayList<List<Double>>(EXPECTED_DATA_TYPES); for (int i = 0; i < EXPECTED_DATA_TYPES; i++) { values.add(new java.util.ArrayList<Double>(8)); } while ((line = in.readLine()) != null) { if (line.startsWith("-")) { break; } line = line.trim(); String[] data = DATA_SPLITTER.split(line); if (types.isEmpty()) { // first record; parse out disk names and use those as fields String disk = data[0].trim(); int n = 1; // name already seen, add the counter if (nameCounts.containsKey(disk)) { n = nameCounts.get(disk) + 1; disk += n; } diskNames.add(DataHelper.newString(disk)); nameCounts.put(disk, n); } // transpose values so each data type will have the data for all disks values.get(0).add(parseValue(data[1], 1)); values.get(1).add(parseValue(data[2], 2)); values.get(2).add(parseValue(data[3], 3)); values.get(3).add(parseValue(data[4], 4)); values.get(4).add(parseValue(data[5], 5)); values.get(5).add(parseValue(data[6], 6)); } // first record complete => all disk names are available to create the types if (types.isEmpty()) { nameCounts = null; if (diskNames.isEmpty()) { throw new IOException("no disks defined in the first data record"); } LOGGER.trace("found {} disks: {}", diskNames.size(), diskNames); String[] fields = new String[diskNames.size()]; diskNames.toArray(fields); types.add(new SubDataType("IOStat ZPool", "alloc", "Capacity Allocated", false, fields)); types.add(new SubDataType("IOStat ZPool", "free", "Capacity Free", false, fields)); types.add(new SubDataType("IOStat ZPool", "r/s", "reads " + "per second", false, fields)); types.add(new SubDataType("IOStat ZPool", "w/s", "writes " + "per second", false, fields)); types.add(new SubDataType("IOStat ZPool", "rMB/s", "MB read " + "per second", false, fields)); types.add(new SubDataType("IOStat ZPool", "wMB/s", "MB write " + "per second", false, fields)); for (DataType type : types) { data.addType(type); } diskNames = null; } // record complete, add the transposed values to each type for (int i = 0; i < types.size(); i++) { List<Double> data = values.get(i); double[] temp = new double[data.size()]; for (int j = 0; j < data.size(); j++) { temp[j] = data.get(j); } record.addData(types.get(i), temp); } // blank record separator line discardLine(); data.addRecord(record); } return data; } finally { if (in != null) { try { in.close(); } catch (Exception e) { // ignore } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Parse complete for {} in {}ms", data.getSourceFile(), (System.nanoTime() - start) / 1000000.0d); } in = null; data = null; types = null; } } } private void discardLine() throws IOException { String line = in.readLine(); if (line == null) { LOGGER.warn("unexpected end of file at line {}; data may be incomplete"); } } private double parseValue(String value, int column) { if ("-".equals(value)) { return Double.NaN; } // operations in thousands, everything else in MB int multiplier = 1024; if ((column == 3) || (column == 4)) { multiplier = 1000; } // if the last char is not a number, get the power of 1024 / 1000 it represents char last = value.charAt(value.length() - 1); int power = 0; switch (last) { case 'K': power = 1; value = value.substring(0, value.length() - 1); break; case 'M': power = 2; value = value.substring(0, value.length() - 1); break; case 'G': power = 3; value = value.substring(0, value.length() - 1); break; case 'T': power = 4; value = value.substring(0, value.length() - 1); break; case 'P': power = 5; value = value.substring(0, value.length() - 1); break; case 'E': power = 6; value = value.substring(0, value.length() - 1); break; case 'Z': power = 7; value = value.substring(0, value.length() - 1); break; case 'Y': power = 8; value = value.substring(0, value.length() - 1); break; // default, do nothing } try { double toReturn = Double.parseDouble(value) * Math.pow(multiplier, power); // capacity in GB if ((column == 1) || (column == 2)) { toReturn /= 1024 * 1024 * 1024; } // bandwidth in MB if ((column == 5) || (column == 6)) { toReturn /= 1024 * 1024; } return toReturn; } catch (NumberFormatException nfe) { LOGGER.warn("invalid numeric data '{}' at line {}, column {}", value, in.getLineNumber(), column); return Double.NaN; } } }