/* * Copyright 2015 herd contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.finra.herd.dao; import static org.junit.Assert.assertTrue; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.UUID; import javax.sql.DataSource; import org.apache.commons.io.IOUtils; import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.finra.herd.dao.config.DaoEnvTestSpringModuleConfig; import org.finra.herd.dao.config.DaoSpringModuleConfig; import org.finra.herd.model.dto.ConfigurationValue; import org.finra.herd.model.jpa.ConfigurationEntity; /** * Tests the Log4jOverridableConfigurer class. Note that the tests that setup Log4J configurations in the database are using low level JDBC to ensure data is * committed to the database before the test runs. Otherwise, the data wouldn't be visible to the test driver since Log4jOverridableConfigurer uses it's own * data source and connection which only sees previously committed data. */ public class Log4jOverridableConfigurerTest extends AbstractDaoTest { private static final String LOG4J_CONFIG_FILENAME = "classpath:log4jOverridableConfigurer-log4j.xml"; private static final String LOG4J_CONFIG_NO_CLOB_FILENAME = "classpath:log4jOverridableConfigurerNoClob-log4j.xml"; private static final String LOG4J_FILENAME_TOKEN = "~log4jFileLocation~"; private static final Logger LOGGER = LoggerFactory.getLogger(Log4jOverridableConfigurerTest.class); @Autowired private ApplicationContext applicationContext; @Test public void testLog4JDbOverrideConfigurationClob() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); String configKey = null; try { // Create a random configuration key to use when inserting the Log4J configuration into the database. configKey = ConfigurationValue.LOG4J_OVERRIDE_CONFIGURATION.getKey() + UUID.randomUUID().toString().substring(0, 5); // Insert the Log4J configuration into the database using the CLOB column. insertDbLog4JConfigurationFromResourceLocation(LOG4J_CONFIG_FILENAME, 0, outputPath, ConfigurationEntity.COLUMN_VALUE_CLOB, configKey); // Shutdown the previously configured logging so we can reinitialize it below. loggingHelper.shutdownLogging(); // Setup the Log4J overridable configurer to use the database location - using the standard CLOB column. Log4jOverridableConfigurer log4jConfigurer = getLog4jOverridableConfigurerForDb(configKey); log4jConfigurer.postProcessBeforeInitialization(null, null); // The database override location does exist and should create a log file. assertTrue("Log4J output file doesn't exist, but should.", Files.exists(outputPath)); } finally { cleanup(configPath, outputPath); deleteDbLog4JConfiguration(configKey); } } @Test public void testLog4JDbOverrideConfigurationNoClob() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); String configKey = null; try { // Create a random configuration key to use when inserting the Log4J configuration into the database. configKey = ConfigurationValue.LOG4J_OVERRIDE_CONFIGURATION.getKey() + UUID.randomUUID().toString().substring(0, 5); // Insert the Log4J configuration into the database using the non-CLOB column. insertDbLog4JConfigurationFromResourceLocation(LOG4J_CONFIG_NO_CLOB_FILENAME, 0, outputPath, ConfigurationEntity.COLUMN_VALUE, configKey); // Shutdown the previously configured logging so we can reinitialize it below. loggingHelper.shutdownLogging(); // Setup the Log4J overridable configurer to use the database location, but override the select column to use the non-CLOB column. Log4jOverridableConfigurer log4jConfigurer = getLog4jOverridableConfigurerForDb(configKey); log4jConfigurer.setSelectColumn(ConfigurationEntity.COLUMN_VALUE); log4jConfigurer.postProcessBeforeInitialization(null, null); // The database override location does exist and should create a log file. assertTrue("Log4J output file doesn't exist, but should.", Files.exists(outputPath)); } finally { cleanup(configPath, outputPath); deleteDbLog4JConfiguration(configKey); } } @Test public void testLog4JDbWithRefreshInterval() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); String configKey = null; try { // Create a random configuration key to use when inserting the Log4J configuration into the database. configKey = ConfigurationValue.LOG4J_OVERRIDE_CONFIGURATION.getKey() + UUID.randomUUID().toString().substring(0, 5); // Insert the standard JUnit Log4J configuration that won't create an output file into the database using the CLOB column. // We will use a monitoring interval of 1 second to ensure the watch dog thread gets executed. insertDbLog4JConfigurationFromResourceLocation(DaoEnvTestSpringModuleConfig.TEST_LOG4J_CONFIG_RESOURCE_LOCATION, 1, outputPath, ConfigurationEntity.COLUMN_VALUE_CLOB, configKey); // Shutdown the previously configured logging so we can reinitialize it below. loggingHelper.shutdownLogging(); // Initialize Log4J with a refresh interval of 1/2 second. This will cause Log4J to check for configuration updates every second. Log4jOverridableConfigurer log4jConfigurer = getLog4jOverridableConfigurerForDb(configKey); log4jConfigurer.postProcessBeforeInitialization(null, null); // First ensure that the Log4J output file doesn't exist. assertTrue("Log4J output file exists, but shouldn't.", Files.notExists(outputPath)); // Update the Log4J configuration with one that will create a log file. updateDbLog4JConfigurationFromResourceLocation(LOG4J_CONFIG_FILENAME, outputPath, ConfigurationEntity.COLUMN_VALUE_CLOB, configKey); // Sleep 3 seconds which will give our watch dog thread and Log4J a chance to read the new configuration file which should create an output file. Thread.sleep(3000); // Ensure that the Log4J output file now exists. assertTrue("Log4J output file doesn't exist, but should.", Files.exists(outputPath)); } finally { cleanup(configPath, outputPath); deleteDbLog4JConfiguration(configKey); } } @Test public void testLog4JNonExistentOverrideLocation() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); try { // Write a Log4J configuration file that will create a random output file. writeFileFromResourceLocation(LOG4J_CONFIG_FILENAME, configPath, outputPath, 0); Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setApplicationContext(applicationContext); log4jConfigurer.setDefaultResourceLocation(DaoEnvTestSpringModuleConfig.TEST_LOG4J_CONFIG_RESOURCE_LOCATION); log4jConfigurer.setOverrideResourceLocation("non_existent_override_location"); log4jConfigurer.postProcessBeforeInitialization(null, null); // Since an override location doesn't exist, the default location which doesn't create a log file will get used and the override log file won't get // created. assertTrue("Log4J output file exists, but shouldn't.", Files.notExists(outputPath)); } finally { cleanup(configPath, outputPath); } } @Test public void testLog4JExistentOverrideLocation() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); try { // Write a Log4J configuration file that will create a random output file. writeFileFromResourceLocation(LOG4J_CONFIG_FILENAME, configPath, outputPath, 1); loggingHelper.shutdownLogging(); Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setApplicationContext(applicationContext); log4jConfigurer.setDefaultResourceLocation(DaoEnvTestSpringModuleConfig.TEST_LOG4J_CONFIG_RESOURCE_LOCATION); log4jConfigurer.setOverrideResourceLocation(configPath.toAbsolutePath().toUri().toURL().toString()); log4jConfigurer.postProcessBeforeInitialization(null, null); // The override location does exist and should create a log file. assertTrue("Log4J output file doesn't exist, but should.", Files.exists(outputPath)); } finally { cleanup(configPath, outputPath); } } @Test public void testLog4JNoOverrideLocationOrDefaultLocation() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); try { // Write a Log4J configuration file that will create a random output file. writeFileFromResourceLocation(LOG4J_CONFIG_FILENAME, configPath, outputPath, 0); Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setApplicationContext(applicationContext); log4jConfigurer.postProcessBeforeInitialization(null, null); // There is no database, override, or default location. This will display an error, but no logging will be configured. // An error will display on system.err which we don't have a way to check so we'll at least ensure that a Log4J output file didn't get created. assertTrue("Log4J output file exists, but shouldn't.", Files.notExists(outputPath)); } finally { cleanup(configPath, outputPath); } } @Test public void testLog4JBlankOverrideLocationAndDefaultLocation() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); try { // Write a Log4J configuration file that will create a random output file. writeFileFromResourceLocation(LOG4J_CONFIG_FILENAME, configPath, outputPath, 0); Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setApplicationContext(applicationContext); log4jConfigurer.setDefaultResourceLocation(" "); log4jConfigurer.setOverrideResourceLocation(" "); log4jConfigurer.postProcessBeforeInitialization(null, null); // There is a blank default and override location so no log file will get created. // An error will display on system.err which we don't have a way to check so we'll at least ensure that a Log4J output file didn't get created. assertTrue("Log4J output file exists, but shouldn't.", Files.notExists(outputPath)); } finally { cleanup(configPath, outputPath); } } @Test public void testLog4JNonExistentOverrideLocationAndDefaultLocation() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); try { // Write a Log4J configuration file that will create a random output file. writeFileFromResourceLocation(LOG4J_CONFIG_FILENAME, configPath, outputPath, 0); Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setApplicationContext(applicationContext); log4jConfigurer.setDefaultResourceLocation("non_existent_default_location"); log4jConfigurer.setOverrideResourceLocation("non_existent_override_location"); log4jConfigurer.postProcessBeforeInitialization(null, null); // This is similar to testLog4JNoLocationOrDefaultLocation except we are specifying explicit invalid locations instead of no locations. assertTrue("Log4J output file exists, but shouldn't.", Files.notExists(outputPath)); } finally { cleanup(configPath, outputPath); } } @Test @Ignore // This works locally, but fails in Jenkins for some reason. We'll need to investigate at some point. public void testLog4JFileWithRefreshInterval() throws Exception { Path configPath = getRandomLog4jConfigPath(); Path outputPath = getRandomLog4jOutputPath(); try { // Write the standard JUnit Log4J configuration that won't create an output file. writeFileFromResourceLocation(DaoEnvTestSpringModuleConfig.TEST_LOG4J_CONFIG_RESOURCE_LOCATION, configPath, outputPath, 1); // Initialize Log4J with a refresh interval of 1/2 second. This will cause Log4J to check for configuration updates every second. Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setApplicationContext(applicationContext); log4jConfigurer.setDefaultResourceLocation(DaoEnvTestSpringModuleConfig.TEST_LOG4J_CONFIG_RESOURCE_LOCATION); log4jConfigurer.setOverrideResourceLocation(configPath.toAbsolutePath().toUri().toURL().toString()); log4jConfigurer.postProcessBeforeInitialization(null, null); // First ensure that the Log4J output file doesn't exist. assertTrue("Log4J output file exists, but shouldn't.", Files.notExists(outputPath)); // Replace the Log4J configuration file with the one that will create an output file. writeFileFromResourceLocation(LOG4J_CONFIG_FILENAME, configPath, outputPath, 1); // Sleep one second which will give Log4J a chance to read the new configuration file which should create an output file. Thread.sleep(3000); // Ensure that the Log4J output file now exists. assertTrue("Log4J output file doesn't exist, but should.", Files.exists(outputPath)); } finally { cleanup(configPath, outputPath); } } /** * Gets a random Log4J configuration path. * * @return the Log4J configuration path. */ private Path getRandomLog4jConfigPath() { return Paths .get(System.getProperty("java.io.tmpdir"), "log4jOverridableConfigurerConfig-log4j-" + String.valueOf(UUID.randomUUID()).substring(0, 5) + ".xml"); } /** * Gets a random Log4J output path. * * @return the Log4J output path. */ private Path getRandomLog4jOutputPath() { return Paths .get(System.getProperty("java.io.tmpdir"), "log4jOverridableConfigurerOutput-log4j-" + String.valueOf(UUID.randomUUID()).substring(0, 5) + ".log"); } /** * Reads the contents of the resource location, substitutes the filename token (if it exists), and writes the contents of the resource to the local file * system. * * @param resourceLocation the resource location of the Log4J configuration. * @param configPath the Log4J configuration path. * @param outputPath the Log4J output path. * @param refreshInterval the refresh interval in seconds. * * @throws Exception if the file couldn't be written. */ private void writeFileFromResourceLocation(String resourceLocation, Path configPath, Path outputPath, int refreshInterval) throws Exception { // Get the Log4J configuration contents from the classpath file. String log4JFileContents = IOUtils.toString(resourceLoader.getResource(resourceLocation).getInputStream()); // Change the tokenized output filename (if it exists) and replace it with a random filename to support multiple invocations of the JUnit. log4JFileContents = log4JFileContents.replace(LOG4J_FILENAME_TOKEN, outputPath.toAbsolutePath().toString().replace("\\", "/")); // Update the refresh interval to 1 second. log4JFileContents = log4JFileContents.replace("monitorInterval=\"0\"", "monitorInterval=\"" + refreshInterval + "\""); // Write the Log4J configuration to the temporary file. try (FileOutputStream fileOutputStream = new FileOutputStream(configPath.toAbsolutePath().toString())) { IOUtils.write(log4JFileContents, fileOutputStream); } } /** * Reads the contents of the resource location, substitutes the filename token (if it exists), and inserts the contents of the resource to the database. * * @param resourceLocation the resource location of the Log4J configuration. * @param monitorInterval the monitor interval in seconds to use when writing the configuration file to the database. * @param outputPath the Log4J output path. * @param log4jConfigurationColumn the column name for the Log4J configuration column * @param configEntityKey the configuration entity key. * * @throws Exception if the file contents couldn't be read or the database record couldn't be inserted. */ private void insertDbLog4JConfigurationFromResourceLocation(String resourceLocation, int monitorInterval, Path outputPath, String log4jConfigurationColumn, String configEntityKey) throws Exception { // Get the Log4J configuration contents from the classpath file. String log4JFileContents = IOUtils.toString(resourceLoader.getResource(resourceLocation).getInputStream()); // Change the tokenized output filename (if it exists) and replace it with a random filename to support multiple invocations of the JUnit. log4JFileContents = log4JFileContents.replace(LOG4J_FILENAME_TOKEN, outputPath.toAbsolutePath().toString().replace("\\", "/")); // Change the monitor interval. log4JFileContents = log4JFileContents.replace("monitorInterval=\"0\"", "monitorInterval=\"" + String.valueOf(monitorInterval) + "\""); // Insert the data. String sql = String.format("INSERT INTO %s (%s, %s) VALUES (?,?)", ConfigurationEntity.TABLE_NAME, ConfigurationEntity.COLUMN_KEY, log4jConfigurationColumn); executePreparedStatement(sql, configEntityKey, log4JFileContents); } /** * Reads the contents of the resource location, substitutes the filename token (if it exists), and inserts the contents of the resource to the database. * * @param resourceLocation the resource location of the Log4J configuration. * @param outputPath the Log4J output path. * @param log4jConfigurationColumn the column name for the Log4J configuration column * @param configEntityKey the configuration entity key. * * @throws Exception if the file contents couldn't be read or the database record couldn't be inserted. */ private void updateDbLog4JConfigurationFromResourceLocation(String resourceLocation, Path outputPath, String log4jConfigurationColumn, String configEntityKey) throws Exception { // Get the Log4J configuration contents from the classpath file. String log4JFileContents = IOUtils.toString(resourceLoader.getResource(resourceLocation).getInputStream()); // Update the refresh interval to 1 second. log4JFileContents = log4JFileContents.replace("monitorInterval=\"0\"", "monitorInterval=\"1\""); // Change the tokenized output filename (if it exists) and replace it with a random filename to support multiple invocations of the JUnit. log4JFileContents = log4JFileContents.replace(LOG4J_FILENAME_TOKEN, outputPath.toAbsolutePath().toString().replace("\\", "/")); // Update the data. String sql = String.format("UPDATE %s SET %s=? WHERE %s=?", ConfigurationEntity.TABLE_NAME, log4jConfigurationColumn, ConfigurationEntity.COLUMN_KEY); executePreparedStatement(sql, log4JFileContents, configEntityKey); } /** * Deletes the configuration entity record from the database. * * @param configEntityKey the configuration entity key to delete. * * @throws SQLException if any SQL errors were encountered. */ private void deleteDbLog4JConfiguration(String configEntityKey) throws SQLException { if (configEntityKey != null) { String sql = String.format("DELETE FROM %s WHERE %s = ?", ConfigurationEntity.TABLE_NAME, ConfigurationEntity.COLUMN_KEY); executePreparedStatement(sql, configEntityKey); } } /** * Executes a SQL prepared statement with the specified arguments. * * @param sql the SQL statement. * @param arguments the arguments. * * @throws SQLException if any SQL errors were encountered. */ private void executePreparedStatement(String sql, Object... arguments) throws SQLException { Connection connection = null; PreparedStatement preparedStatement = null; try { DataSource dataSource = DaoSpringModuleConfig.getHerdDataSource(); connection = dataSource.getConnection(); preparedStatement = connection.prepareStatement(sql); for (int i = 0; i < arguments.length; i++) { preparedStatement.setObject(i + 1, arguments[i]); } preparedStatement.execute(); } finally { if (preparedStatement != null) { preparedStatement.close(); } if (connection != null) { connection.close(); } } } /** * Gets a newly created Log4J overridable configurer for a database. * * @param configKey the configuration key to use as the where value. * * @return the Log4J overridable configurer. */ private Log4jOverridableConfigurer getLog4jOverridableConfigurerForDb(String configKey) { Log4jOverridableConfigurer log4jConfigurer = new Log4jOverridableConfigurer(); log4jConfigurer.setTableName(ConfigurationEntity.TABLE_NAME); log4jConfigurer.setSelectColumn(ConfigurationEntity.COLUMN_VALUE_CLOB); log4jConfigurer.setWhereColumn(ConfigurationEntity.COLUMN_KEY); log4jConfigurer.setWhereValue(configKey); log4jConfigurer.setDataSource(DaoSpringModuleConfig.getHerdDataSource()); log4jConfigurer.setApplicationContext(applicationContext); return log4jConfigurer; } /** * Cleanup the Log4J files created. * * @param configPath the configuration path. * @param outputPath the output path. * * @throws IOException if any problems were encountered while cleaning up the files. */ private void cleanup(Path configPath, Path outputPath) throws IOException { // Shutdown the logging which will release the lock on the output file. loggingHelper.shutdownLogging(); // Delete the Log4J configuration we created in the setup. if (Files.exists(configPath)) { Files.delete(configPath); } // If we created a Log4J output file (not always), then delete it. if (Files.exists(outputPath)) { Files.delete(outputPath); } } }