package de.is24.util.monitoring.database; import de.is24.util.monitoring.Counter; import de.is24.util.monitoring.InApplicationMonitor; import de.is24.util.monitoring.InApplicationMonitorRule; import de.is24.util.monitoring.Reportable; import de.is24.util.monitoring.ReportableObserver; import de.is24.util.monitoring.StateValueProvider; import de.is24.util.monitoring.Timer; import de.is24.util.monitoring.database.MonitoringDataSource.SqlExceptionPredicate; import org.apache.log4j.Appender; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.easymock.EasyMock; import org.easymock.EasyMockSupport; import org.easymock.IArgumentMatcher; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; import java.sql.Clob; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.NClob; import java.sql.PreparedStatement; import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLWarning; import java.sql.SQLXML; import java.sql.Savepoint; import java.sql.Statement; import java.sql.Struct; import java.util.Map; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; /** * Tests the behaviour of {@link MonitoringDataSource}. * * @author <a href="mailto:SKirsch@is24.de">Sebastian Kirsch</a> */ public final class MonitoringDataSourceTest extends EasyMockSupport { @Rule public final InApplicationMonitorRule inApplicationMonitorRule = new InApplicationMonitorRule(); private static LoggingEvent logEventMatches(Level level, String messagePattern) { EasyMock.reportMatcher(new LogEventMatches(level, messagePattern)); return null; } /** The observer is used to hook into AppMon4J. */ private AppMon4JObserver observer; /** * Asserts that the monitor has the specified value. * * @param monitorName * the name of the monitor value to check * @param expectedValue * the expected value */ private void assertMonitor(String monitorName, long expectedValue) { Reportable reportable = this.observer.reportables.get(monitorName); long actualCount = 0; if (expectedValue > 0) { Assert.assertNotNull("There is no counter for [" + monitorName + "]!", reportable); if (reportable instanceof Counter) { actualCount = ((Counter) reportable).getCount(); } else if (reportable instanceof Timer) { actualCount = ((Timer) reportable).getCount(); } else if (reportable instanceof StateValueProvider) { actualCount = ((StateValueProvider) reportable).getValue(); } else { actualCount = -1; } } Assert.assertEquals("Wrong count for monitor name [" + monitorName + "]!", expectedValue, actualCount); } private String expectLogEventWithLevelContaining(Level level, String message) { Appender appenderMock = givenAnAppenderAtTheMonitoringDataSourceLogger(); appenderMock.doAppend(logEventMatches(level, "^.*" + message + ".*$")); expectLastCall(); return message; } private String expectWarnLogEventCotaining(String message) { return expectLogEventWithLevelContaining(Level.WARN, message); } private Appender givenAnAppenderAtTheMonitoringDataSourceLogger() { Appender appenderMock = createMock(Appender.class); Logger logger = Logger.getLogger(MonitoringDataSource.class); logger.addAppender(appenderMock); return appenderMock; } /** Set up the test case. */ @Before public void setUp() { this.observer = new AppMon4JObserver(); InApplicationMonitor.getInstance().getCorePlugin().addReportableObserver(this.observer); } /** Clean up. */ @After public void tearDown() { resetAll(); } @Test public void shouldLogExecutedSqlWhenExceptionWasThrownForStatement() throws SQLException { String sql = expectWarnLogEventCotaining("SELECT 1 FROM DUAL"); DataSource datasourceMock = createMock(DataSource.class); Connection connectionMock = createMock(Connection.class); Statement statementMock = createMock(Statement.class); expect(datasourceMock.getConnection()).andReturn(connectionMock); expect(connectionMock.createStatement()).andReturn(statementMock); expect(statementMock.executeQuery(sql)).andThrow(new SQLException( "Somebody set up us the bomb!")); MonitoringDataSource objectUnderTest = new MonitoringDataSource(datasourceMock); replayAll(); Connection connection = objectUnderTest.getConnection(); Statement statement = connection.createStatement(); try { statement.executeQuery(sql); fail("Expectations have been screwed!"); } catch (SQLException expected) { } verifyAll(); } @Test public void shouldLogExecutedSqlWhenExceptionWasThrownForPreparedStatement() throws SQLException { String sql = expectWarnLogEventCotaining("SELECT 1 FROM DUAL"); DataSource datasourceMock = createMock(DataSource.class); Connection connectionMock = createMock(Connection.class); PreparedStatement statementMock = createMock(PreparedStatement.class); expect(datasourceMock.getConnection()).andReturn(connectionMock); expect(connectionMock.prepareStatement(sql)).andReturn(statementMock); expect(statementMock.executeQuery()).andThrow(new SQLException( "Somebody set up us the bomb!")); MonitoringDataSource objectUnderTest = new MonitoringDataSource(datasourceMock); replayAll(); Connection connection = objectUnderTest.getConnection(); PreparedStatement statement = connection.prepareStatement(sql); try { statement.executeQuery(); fail("Expectations have been screwed!"); } catch (SQLException expected) { } verifyAll(); } @Test public void shouldNotLogExecutedSqlWhenExceptionWasThrownDuringClose() throws SQLException { givenAnAppenderAtTheMonitoringDataSourceLogger(); String sql = "SELECT 1 FROM DUAL"; DataSource datasourceMock = createMock(DataSource.class); Connection connectionMock = createMock(Connection.class); PreparedStatement statementMock = createMock(PreparedStatement.class); expect(datasourceMock.getConnection()).andReturn(connectionMock); expect(connectionMock.prepareStatement(sql)).andReturn(statementMock); statementMock.close(); expectLastCall().andThrow(new SQLException("Somebody set up us the bomb!")); MonitoringDataSource objectUnderTest = new MonitoringDataSource(datasourceMock); replayAll(); Connection connection = objectUnderTest.getConnection(); Statement statement = connection.prepareStatement(sql); try { statement.close(); fail("Expectations have been screwed!"); } catch (SQLException expected) { } verifyAll(); } @Test public void shouldLogFilteredSqlExceptionAtInfoLevel() throws SQLException { String sql = expectLogEventWithLevelContaining(Level.INFO, "SELECT 1 FROM DUAL"); DataSource datasourceMock = createMock(DataSource.class); Connection connectionMock = createMock(Connection.class); Statement statementMock = createMock(Statement.class); expect(datasourceMock.getConnection()).andReturn(connectionMock); expect(connectionMock.createStatement()).andReturn(statementMock); expect(statementMock.executeQuery(sql)).andThrow(new SQLException( "Somebody set up us the bomb!", "SomeState", 42)); MonitoringDataSource objectUnderTest = new MonitoringDataSource(datasourceMock); objectUnderTest.addExceptionLogFilter(new MonitoringDataSource.Predicate<SQLException>() { @Override public boolean apply(SQLException input) { return input.getErrorCode() == 42; } }); replayAll(); Connection connection = objectUnderTest.getConnection(); Statement statement = connection.createStatement(); try { statement.executeQuery(sql); fail("Expectations have been screwed!"); } catch (SQLException expected) { } verifyAll(); } @Test public void shouldRecognizeExceptionLogFiltersAppropriately() throws SQLException { String sql = expectLogEventWithLevelContaining(Level.INFO, "SELECT 1 FROM DUAL"); DataSource datasourceMock = createMock(DataSource.class); Connection connectionMock = createMock(Connection.class); Statement statementMock = createMock(Statement.class); expect(datasourceMock.getConnection()).andReturn(connectionMock); expect(connectionMock.createStatement()).andReturn(statementMock); expect(statementMock.executeQuery(sql)).andThrow(new SQLException( "Somebody set up us the bomb!", "SomeState", 42)); MonitoringDataSource objectUnderTest = new MonitoringDataSource(datasourceMock); objectUnderTest.setExceptionLogFilters("1,42:^.*bomb.*$"); replayAll(); Connection connection = objectUnderTest.getConnection(); Statement statement = connection.createStatement(); try { statement.executeQuery(sql); fail("Expectations have been screwed!"); } catch (SQLException expected) { } verifyAll(); } @Test public void shouldFilterErrorCodeOfSqlExceptionsCorrectly() { SqlExceptionPredicate objectUnderTest = new MonitoringDataSource.SqlExceptionPredicate(1, null); assertTrue(objectUnderTest.apply(new SQLException("foo", "bar", 1))); assertTrue(objectUnderTest.apply(new SQLException("foo", null, 1))); assertTrue(objectUnderTest.apply(new SQLException(null, null, 1))); assertFalse(objectUnderTest.apply(new SQLException())); assertFalse(objectUnderTest.apply(new SQLException("foo"))); assertFalse(objectUnderTest.apply(new SQLException("foo", "bar", 42))); } @Test public void shouldFilterErrorCodeAndMessageRegexOfSqlExceptionsCorrectly() { SqlExceptionPredicate objectUnderTest = new MonitoringDataSource.SqlExceptionPredicate(42, "^.*foo.*$"); assertTrue(objectUnderTest.apply(new SQLException("blafoobla", "bar", 42))); assertTrue(objectUnderTest.apply(new SQLException("foo", "bar", 42))); assertTrue(objectUnderTest.apply(new SQLException("foo", null, 42))); assertFalse(objectUnderTest.apply(new SQLException(null, null, 42))); assertFalse(objectUnderTest.apply(new SQLException())); assertFalse(objectUnderTest.apply(new SQLException("foo"))); assertFalse(objectUnderTest.apply(new SQLException("foo", "bar", 1))); } /** Verifies that the {@link MonitoringDataSource} makes the appropriate AppMon4J calls. * * @throws java.sql.SQLException */ @Test public void checkErrorCounters() throws SQLException { AtomicBoolean errorSwitch = new AtomicBoolean(false); String baseName = "checkErrorCounter"; MonitoringDataSource dataSource = new MonitoringDataSource(new MockDataSource(errorSwitch), baseName); assertMonitor(MonitoringDataSource.class.getName() + ".maxOpenConnections", 0); Connection c = dataSource.getConnection(); errorSwitch.set(true); try { dataSource.getConnection(); Assert.fail("Calling getConnection() should fail!"); } catch (SQLException sqlE) { // we expect that } try { c.commit(); Assert.fail("Calling commit() should fail!"); } catch (SQLException sqlE) { // we expect that } try { c.rollback(); Assert.fail("Calling rollback() should fail!"); } catch (SQLException sqlE) { // we expect that } try { c.rollback(null); Assert.fail("Calling rollback(Savepoint) should fail!"); } catch (SQLException sqlE) { // we expect that } c.close(); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.getConnection", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".getConnection", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".getPersonalisedConnection", 0); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.commit", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.rollback", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.rollbackSavepoint", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".close", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".maxOpenConnections", 1); errorSwitch.set(false); c = dataSource.getConnection("me", "secret"); errorSwitch.set(true); try { dataSource.getConnection("me", "secret"); Assert.fail("Calling getConnection(String, String) should fail!"); } catch (SQLException sqlE) { // we expect that } try { c.commit(); Assert.fail("Calling commit() should fail!"); } catch (SQLException sqlE) { // we expect that } try { c.rollback(); Assert.fail("Calling rollback() should fail!"); } catch (SQLException sqlE) { // we expect that } try { c.rollback(null); Assert.fail("Calling rollback(Savepoint) should fail!"); } catch (SQLException sqlE) { // we expect that } c.close(); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.getConnection", 2); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".getConnection", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".getPersonalisedConnection", 1); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.commit", 2); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.rollback", 2); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".error.rollbackSavepoint", 2); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".close", 2); assertMonitor(MonitoringDataSource.class.getName() + "." + baseName + ".maxOpenConnections", 1); } /** * Used to hook into AppMon4J for JUnit tests. * * @author <a href="mailto:SKirsch@is24.de">Sebastian Kirsch</a> * @see MonitoringDataSourceTest */ private static class AppMon4JObserver implements ReportableObserver { public final ConcurrentHashMap<String, Reportable> reportables = new ConcurrentHashMap<String, Reportable>(); public void addNewReportable(Reportable reportable) { reportables.put(reportable.getName(), reportable); } } /** * Mocks a {@link javax.sql.DataSource} for JUnit tests. * * @author <a href="mailto:SKirsch@is24.de">Sebastian Kirsch</a> * @see MonitoringDataSourceTest */ private static class MockDataSource implements DataSource { private final AtomicBoolean throwError; public MockDataSource(AtomicBoolean throwError) { this.throwError = throwError; } public Connection getConnection() throws SQLException { if (throwError.get()) { throw new SQLException("Somebody set up us the bomb!", "All your base"); } return new MockConnection(this.throwError); } public Connection getConnection(String username, String password) throws SQLException { if (throwError.get()) { throw new SQLException("Somebody set up us the bomb!", "All your base"); } return new MockConnection(this.throwError); } public PrintWriter getLogWriter() throws SQLException { return null; } public int getLoginTimeout() throws SQLException { return 0; } public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } public void setLogWriter(PrintWriter out) throws SQLException { } public void setLoginTimeout(int seconds) throws SQLException { } public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; } public <T> T unwrap(Class<T> iface) throws SQLException { return null; } } /** * Mocks a {@link java.sql.Connection} for JUnit tests. * * @author <a href="mailto:SKirsch@is24.de">Sebastian Kirsch</a> * @see MonitoringDataSourceTest */ private static class MockConnection implements Connection { private final AtomicBoolean throwError; public MockConnection(AtomicBoolean throwError) { this.throwError = throwError; } public void clearWarnings() throws SQLException { } public void close() throws SQLException { } public void commit() throws SQLException { if (throwError.get()) { throw new SQLException("Somebody set up us the bomb!", "All your base"); } } public Statement createStatement() throws SQLException { return null; } public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { return null; } public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return null; } public boolean getAutoCommit() throws SQLException { return false; } public String getCatalog() throws SQLException { return null; } public int getHoldability() throws SQLException { return 0; } public DatabaseMetaData getMetaData() throws SQLException { return null; } public int getTransactionIsolation() throws SQLException { return 0; } public Map<String, Class<?>> getTypeMap() throws SQLException { return null; } public SQLWarning getWarnings() throws SQLException { return null; } public boolean isClosed() throws SQLException { return false; } public boolean isReadOnly() throws SQLException { return false; } public String nativeSQL(String sql) throws SQLException { return null; } public CallableStatement prepareCall(String sql) throws SQLException { return null; } public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return null; } public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return null; } public PreparedStatement prepareStatement(String sql) throws SQLException { return null; } public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { return null; } public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { return null; } public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { return null; } public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { return null; } public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return null; } public void releaseSavepoint(Savepoint savepoint) throws SQLException { } public void rollback() throws SQLException { if (throwError.get()) { throw new SQLException("Somebody set up us the bomb!", "All your base"); } } public void rollback(Savepoint savepoint) throws SQLException { if (throwError.get()) { throw new SQLException("Somebody set up us the bomb!", "All your base"); } } public void setAutoCommit(boolean autoCommit) throws SQLException { } public void setCatalog(String catalog) throws SQLException { } public void setHoldability(int holdability) throws SQLException { } public void setReadOnly(boolean readOnly) throws SQLException { } public Savepoint setSavepoint() throws SQLException { return null; } public Savepoint setSavepoint(String name) throws SQLException { return null; } public void setTransactionIsolation(int level) throws SQLException { } public void setTypeMap(Map<String, Class<?>> map) throws SQLException { } public Array createArrayOf(String typeName, Object[] elements) throws SQLException { return null; } public Blob createBlob() throws SQLException { return null; } public Clob createClob() throws SQLException { return null; } public NClob createNClob() throws SQLException { return null; } public SQLXML createSQLXML() throws SQLException { return null; } public Struct createStruct(String typeName, Object[] attributes) throws SQLException { return null; } public void setSchema(String schema) throws SQLException { } public String getSchema() throws SQLException { return null; } public void abort(Executor executor) throws SQLException { } public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { } public int getNetworkTimeout() throws SQLException { return 0; } public Properties getClientInfo() throws SQLException { return null; } public String getClientInfo(String name) throws SQLException { return null; } public boolean isValid(int timeout) throws SQLException { return false; } public void setClientInfo(Properties properties) throws SQLClientInfoException { } public void setClientInfo(String name, String value) throws SQLClientInfoException { } public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; } public <T> T unwrap(Class<T> iface) throws SQLException { return null; } } private static class LogEventMatches implements IArgumentMatcher { private final Level expectedLevel; private final String messagePattern; public LogEventMatches(Level expectedLevel, String messagePattern) { this.expectedLevel = expectedLevel; if ((messagePattern == null) || (messagePattern.length() == 0)) { throw new IllegalArgumentException("messagePattern must not be empty"); } this.messagePattern = messagePattern; } @Override public void appendTo(StringBuffer buffer) { buffer.append("logEventMatches("); if (this.expectedLevel != null) { buffer.append(this.expectedLevel).append("|"); } buffer.append("\"").append(this.messagePattern).append("\")"); } @Override public boolean matches(Object argument) { if (!LoggingEvent.class.isInstance(argument)) { return false; } LoggingEvent logEvent = (LoggingEvent) argument; if ((this.expectedLevel != null) && !this.expectedLevel.equals(logEvent.getLevel())) { return false; } return (logEvent.getMessage() != null) && logEvent.getMessage().toString().matches(messagePattern); } } }