package com.tesora.dve.sql.parser; /* * #%L * Tesora Inc. * Database Virtualization Engine * %% * Copyright (C) 2011 - 2014 Tesora Inc. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License, version 3, * as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Locale; import com.tesora.dve.sql.parser.filter.LogFileFilter; class MysqlLogLineBuffer extends LogLineBuffer { // for mysql log files the format is: // whitespace connection-id whitespace command-type whitespace command // or // integer whitespace time whitespace conn-id whitespace command-type whitespace command // where command can take up several lines. in all cases a new entry always starts // with the beginning of the line, then the whitespace, etc. // we used to look for the command and then match around it, but we're switching to a regex since // it will likely be easier to avoid false matches. private static final MysqlLogFileEntryKind[] delimiters = MysqlLogFileEntryKind.values(); private static class MatchResult { protected int start; protected int end; protected MysqlLogFileEntryKind delimiter; protected String connID; public MatchResult(int startoff, int endoff, MysqlLogFileEntryKind d, String connStr) { start = startoff; end = endoff; delimiter = d; connID = connStr; } public MysqlLogFileEntryKind getDelimiter() { return delimiter; } public int getStartOffset() { return start; } public int getEndOffset() { return end; } public String getConnectionID() { return connID; } } protected LogFileFilter filter; public MysqlLogLineBuffer() { super(); } public MysqlLogLineBuffer(LogFileFilter filter) { this(); this.filter = filter; } private static final int START = 1; private static final int YEAR = 2; private static final int HOUR = 3; private static final int MINUTE = 4; private static final int SECOND = 5; private static final int NOTIME = 6; // expect \t\t private static final int CONN = 7; private static final int COMMAND = 8; private static final int PRECONN_WS = 9; // for mysql 5.1 private static final int PREHOUR_WS = 10; private LinkedHashSet<MysqlLogFileEntryKind> possibilities = new LinkedHashSet<MysqlLogFileEntryKind>(); private int pthumb = -1; private int connoffset = -1; private String currentConnID = null; private int startedAt = -1; private MatchResult left = null, right = null; private char[] headerBuf = new char[40]; private int headerThumb = -1; private int state; private void processStartState(char c) { headerBuf[++headerThumb] = c; startedAt = buffer.length(); connoffset = -1; if (Character.isDigit(c)) { state = YEAR; } else if ('\t' == c) { state = NOTIME; } else { // not a start headerThumb = -1; startedAt = -1; state = 0; } } private void processNonStartState(char c) { // look at the previous switch(state) { case NOTIME: if ('\t' == c) { state = PRECONN_WS; } else { state = 0; } break; case YEAR: if (Character.isDigit(c)) { // ok } else if (' ' == c) { state = PREHOUR_WS; } else { state = 0; } break; case PREHOUR_WS: if (' ' == c) { // ok } else if (Character.isDigit(c)) { state = HOUR; } else { state = 0; } break; case HOUR: if (Character.isDigit(c)) { // ok } else if (':' == c) { state = MINUTE; } else { state = 0; } break; case MINUTE: if (Character.isDigit(c)) { // ok } else if (':' == c) { state = SECOND; } else { state = 0; } break; case SECOND: if (Character.isDigit(c)) { // ok } else if ('\t' == c || ' ' == c) { state = PRECONN_WS; } else { state = 0; } break; case PRECONN_WS: if ('\t' == c || ' ' == c) { // ok } else if (Character.isDigit(c)) { state = CONN; if (connoffset == -1) connoffset = headerThumb+1; } else { state = 0; } break; case CONN: if (Character.isDigit(c)) { // ok if (connoffset == -1) connoffset = headerThumb+1; } else if (' ' == c && connoffset != -1) { // it's unclear when we enter the conn state and don't set connoffset, but if that is the case, we are confused. // move on. // // figure out the current conn value now currentConnID = new String(headerBuf,connoffset,(headerThumb - connoffset + 1)); state = COMMAND; pthumb = -1; } else { state = 0; } break; case COMMAND: handleCommand(c); break; } if (state != 0) { headerBuf[++headerThumb] = c; } else { // reset all headerThumb = -1; pthumb = -1; currentConnID = null; connoffset = -1; if (right != null && right.getDelimiter() == MysqlLogFileEntryKind.QUIT) { state = START; } } } private void handleCommand(char c) { if (pthumb == -1) { possibilities.clear(); // first character, we have to build the set for(MysqlLogFileEntryKind m : delimiters) { if (c == m.getMatch().charAt(0)) { possibilities.add(m); } } if (possibilities.isEmpty()) { state = 0; } else { pthumb = 1; } } else if ((c == '\t') || (c == '\n')) { // possibly this means we have the whole thing // if we have possibilities left that are longer than pthumb - get rid of them for(Iterator<MysqlLogFileEntryKind> iter = possibilities.iterator(); iter.hasNext(); ) { MysqlLogFileEntryKind m = iter.next(); if (m.getMatch().length() > pthumb) iter.remove(); } if (possibilities.size() > 1) state = 0; else { MysqlLogFileEntryKind currentEntry = possibilities.iterator().next(); MatchResult mr = new MatchResult(startedAt,buffer.length(),currentEntry,currentConnID); if (left == null) left = mr; else if (right == null) right = mr; // we've identified, state is 0 state = 0; } } else { for(Iterator<MysqlLogFileEntryKind> iter = possibilities.iterator(); iter.hasNext();) { MysqlLogFileEntryKind m = iter.next(); if (pthumb < m.getMatch().length() && c == m.getMatch().charAt(pthumb)) { // ok } else { iter.remove(); } } if (possibilities.isEmpty()) { state = 0; } else { pthumb++; } } } @Override protected void add(char c, boolean eol) { if (buffer == null) buffer = new StringBuilder(); buffer.append(c); if (eol && !isHandlingCommand()) { state = START; } else if (state == START) { processStartState(c); } else if (state != 0) { processNonStartState(c); } } protected boolean haveStatement() { if (buffer == null || left == null || right == null) return false; return true; } @Override protected TaggedStatement getStatement() { if (left != null && right != null) { // any statement is between the last match position of the first match and the first match position of the last match. // well, there might be some extra cruft in there so search backwards from the start offset of the second match for // a carriage return String attempt = buffer.toString().substring(left.getEndOffset(), right.getStartOffset()+1); char[] arr = attempt.toCharArray(); int last = arr.length - 1; for(int i = arr.length - 1; i > -1; i--) { char c = arr[i]; if (c == '\n') { last = i; break; } } String stmt = attempt.substring(0,last + 1); // the connection id is just before the first position in stmt - so search backwards from queryPositions[0] // reset the buffer to just before the last Query int connID = -1; try { connID = Integer.parseInt(left.getConnectionID()); } catch (NumberFormatException nfe) { System.err.println("Unable to pick conn id out of " + left.getConnectionID()); connID = -1; } if (left.getDelimiter() == MysqlLogFileEntryKind.QUIT) { stmt = MysqlLogFileEntryKind.QUIT.name(); } // the new buffer starts just before the second match String newbuf = buffer.toString().substring(right.getStartOffset()); buffer = new StringBuilder(); buffer.append(newbuf); MatchResult oldLeft = left; // we have to rejigger right and set it to left left = new MatchResult(0,right.getEndOffset() - right.getStartOffset(),right.getDelimiter(),right.getConnectionID()); right = null; if (stmt.charAt(0) == '#') { // not using it return null; } else if (oldLeft.getDelimiter().ignore()) { return null; } if (filterCurrentLine(connID, stmt)) { return null; } if (oldLeft.getDelimiter().usePayload()) { if (oldLeft.getDelimiter() == MysqlLogFileEntryKind.INITDB) { // initdb is basically a use statement - make it look that way return new TaggedStatement("use " + stmt,connID,oldLeft.getDelimiter()); } else if (oldLeft.getDelimiter() == MysqlLogFileEntryKind.CONNECT) { if (stmt.toLowerCase(Locale.ENGLISH).startsWith("access denied")) { return null; } else if (stmt.toLowerCase(Locale.ENGLISH).indexOf(" on ") > 0) { // if the line ends with "on <word>" then the word is the db int offset = stmt.toLowerCase(Locale.ENGLISH).indexOf(" on ") + 4; char[] buf = stmt.substring(offset).toCharArray(); StringBuilder dbname = new StringBuilder(); for(int i = 0; i < buf.length; i++) { if (Character.isWhitespace(buf[i])) { break; } else { dbname.append(buf[i]); } } String db = dbname.toString(); if ("".equals(db)) return new TaggedStatement("Connect", connID,oldLeft.getDelimiter()); return new TaggedStatement("use " + db, connID,oldLeft.getDelimiter()); } else if (stmt.toLowerCase(Locale.ENGLISH).endsWith(" on")) { // empty connect in this case, treat like a regular connect return new TaggedStatement("Connect", connID,oldLeft.getDelimiter()); } } else if (oldLeft.getDelimiter() == MysqlLogFileEntryKind.QUERY) { if (stmt.startsWith("-")) { // potentially, we're going to fail it, so toss it overboard return null; } } return new TaggedStatement(stmt,connID,oldLeft.getDelimiter()); } else { return new TaggedStatement(oldLeft.getDelimiter().getMatch(), connID,oldLeft.getDelimiter()); } } return null; } boolean filterCurrentLine(int connID, String stmt) { if (filter == null) { return false; } else { return filter.filterLine(connID, stmt); } } private boolean isHandlingCommand() { return pthumb != -1; } }