Repository: logging-log4j2 Updated Branches: refs/heads/release-2.x b06395221 -> 0cd480ff5
[LOG4J2-2509] Allow a JDBC Appender to truncate strings to match a table's metadata column length limit. Not documenting truncateStrings for now since it might make sense to always have it on. Project: http://git-wip-us.apache.org/repos/asf/logging-log4j2/repo Commit: http://git-wip-us.apache.org/repos/asf/logging-log4j2/commit/0cd480ff Tree: http://git-wip-us.apache.org/repos/asf/logging-log4j2/tree/0cd480ff Diff: http://git-wip-us.apache.org/repos/asf/logging-log4j2/diff/0cd480ff Branch: refs/heads/release-2.x Commit: 0cd480ff5c3654e411d42f8ce34c1cf34e43d9cc Parents: b063952 Author: Gary Gregory <garydgreg...@gmail.com> Authored: Fri Nov 16 16:55:11 2018 -0700 Committer: Gary Gregory <garydgreg...@gmail.com> Committed: Fri Nov 16 16:55:11 2018 -0700 ---------------------------------------------------------------------- .../log4j/core/appender/db/ColumnMapping.java | 12 + .../core/appender/db/jdbc/ColumnConfig.java | 274 ++++++++-------- .../core/appender/db/jdbc/JdbcAppender.java | 31 +- .../appender/db/jdbc/JdbcDatabaseManager.java | 328 +++++++++++++++---- .../AbstractJdbcAppenderFactoryMethodTest.java | 17 +- .../JdbcAppenderMapMessageDataSourceTest.java | 27 +- .../src/test/resources/log4j2-jdbc-dbcp2.xml | 4 + src/changes/changes.xml | 3 + 8 files changed, 490 insertions(+), 206 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/ColumnMapping.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/ColumnMapping.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/ColumnMapping.java index 1178156..1219060 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/ColumnMapping.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/ColumnMapping.java @@ -17,6 +17,7 @@ package org.apache.logging.log4j.core.appender.db; import java.util.Date; +import java.util.Locale; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.Core; @@ -188,20 +189,27 @@ public class ColumnMapping { } private static final Logger LOGGER = StatusLogger.getLogger(); + @PluginBuilderFactory public static Builder newBuilder() { return new Builder(); } + public static String toKey(final String name) { + return name.toUpperCase(Locale.ROOT); + } + private final StringLayout layout; private final String literalValue; private final String name; + private final String nameKey; private final String parameter; private final String source; private final Class<?> type; private ColumnMapping(final String name, final String source, final StringLayout layout, final String literalValue, final String parameter, final Class<?> type) { this.name = name; + this.nameKey = toKey(name); this.source = source; this.layout = layout; this.literalValue = literalValue; @@ -221,6 +229,10 @@ public class ColumnMapping { return name; } + public String getNameKey() { + return nameKey; + } + public String getParameter() { return parameter; } http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/ColumnConfig.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/ColumnConfig.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/ColumnConfig.java index ba0ad25..6dc6735 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/ColumnConfig.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/ColumnConfig.java @@ -37,90 +37,6 @@ import org.apache.logging.log4j.util.Strings; */ @Plugin(name = "Column", category = Core.CATEGORY_NAME, printObject = true) public final class ColumnConfig { - private static final Logger LOGGER = StatusLogger.getLogger(); - - private final String columnName; - private final PatternLayout layout; - private final String literalValue; - private final boolean eventTimestamp; - private final boolean unicode; - private final boolean clob; - - private ColumnConfig(final String columnName, final PatternLayout layout, final String literalValue, - final boolean eventDate, final boolean unicode, final boolean clob) { - this.columnName = columnName; - this.layout = layout; - this.literalValue = literalValue; - this.eventTimestamp = eventDate; - this.unicode = unicode; - this.clob = clob; - } - - public String getColumnName() { - return this.columnName; - } - - public PatternLayout getLayout() { - return this.layout; - } - - public String getLiteralValue() { - return this.literalValue; - } - - public boolean isEventTimestamp() { - return this.eventTimestamp; - } - - public boolean isUnicode() { - return this.unicode; - } - - public boolean isClob() { - return this.clob; - } - - @Override - public String toString() { - return "{ name=" + this.columnName + ", layout=" + this.layout + ", literal=" + this.literalValue - + ", timestamp=" + this.eventTimestamp + " }"; - } - - /** - * Factory method for creating a column config within the plugin manager. - * - * @see Builder - * @deprecated use {@link #newBuilder()} - */ - @Deprecated - public static ColumnConfig createColumnConfig(final Configuration config, final String name, final String pattern, - final String literalValue, final String eventTimestamp, - final String unicode, final String clob) { - if (Strings.isEmpty(name)) { - LOGGER.error("The column config is not valid because it does not contain a column name."); - return null; - } - - final boolean isEventTimestamp = Boolean.parseBoolean(eventTimestamp); - final boolean isUnicode = Booleans.parseBoolean(unicode, true); - final boolean isClob = Boolean.parseBoolean(clob); - - return newBuilder() - .setConfiguration(config) - .setName(name) - .setPattern(pattern) - .setLiteral(literalValue) - .setEventTimestamp(isEventTimestamp) - .setUnicode(isUnicode) - .setClob(isClob) - .build(); - } - - @PluginBuilderFactory - public static Builder newBuilder() { - return new Builder(); - } - public static class Builder implements org.apache.logging.log4j.core.util.Builder<ColumnConfig> { @PluginConfiguration @@ -145,34 +61,71 @@ public final class ColumnConfig { @PluginBuilderAttribute private boolean isClob; + @Override + public ColumnConfig build() { + if (Strings.isEmpty(name)) { + LOGGER.error("The column config is not valid because it does not contain a column name."); + return null; + } + + final boolean isPattern = Strings.isNotEmpty(pattern); + final boolean isLiteralValue = Strings.isNotEmpty(literal); + + if ((isPattern && isLiteralValue) || (isPattern && isEventTimestamp) || (isLiteralValue && isEventTimestamp)) { + LOGGER.error("The pattern, literal, and isEventTimestamp attributes are mutually exclusive."); + return null; + } + + if (isEventTimestamp) { + return new ColumnConfig(name, null, null, true, false, false); + } + + if (isLiteralValue) { + return new ColumnConfig(name, null, literal, false, false, false); + } + + if (isPattern) { + final PatternLayout layout = + PatternLayout.newBuilder() + .withPattern(pattern) + .withConfiguration(configuration) + .withAlwaysWriteExceptions(false) + .build(); + return new ColumnConfig(name, layout, null, false, isUnicode, isClob); + } + + LOGGER.error("To configure a column you must specify a pattern or literal or set isEventDate to true."); + return null; + } + /** - * The configuration object. + * If {@code "true"}, indicates that the column is a character LOB (CLOB). * * @return this. */ - public Builder setConfiguration(final Configuration configuration) { - this.configuration = configuration; + public Builder setClob(final boolean clob) { + isClob = clob; return this; } /** - * The name of the database column as it exists within the database table. + * The configuration object. * * @return this. */ - public Builder setName(final String name) { - this.name = name; + public Builder setConfiguration(final Configuration configuration) { + this.configuration = configuration; return this; } /** - * The {@link PatternLayout} pattern to insert in this column. Mutually exclusive with - * {@code literal!=null} and {@code eventTimestamp=true} + * If {@code "true"}, indicates that this column is a date-time column in which the event timestamp should be + * inserted. Mutually exclusive with {@code pattern!=null} and {@code literal!=null}. * * @return this. */ - public Builder setPattern(final String pattern) { - this.pattern = pattern; + public Builder setEventTimestamp(final boolean eventTimestamp) { + isEventTimestamp = eventTimestamp; return this; } @@ -188,71 +141,124 @@ public final class ColumnConfig { } /** - * If {@code "true"}, indicates that this column is a date-time column in which the event timestamp should be - * inserted. Mutually exclusive with {@code pattern!=null} and {@code literal!=null}. + * The name of the database column as it exists within the database table. * * @return this. */ - public Builder setEventTimestamp(final boolean eventTimestamp) { - isEventTimestamp = eventTimestamp; + public Builder setName(final String name) { + this.name = name; return this; } /** - * If {@code "true"}, indicates that the column is a Unicode String. + * The {@link PatternLayout} pattern to insert in this column. Mutually exclusive with + * {@code literal!=null} and {@code eventTimestamp=true} * * @return this. */ - public Builder setUnicode(final boolean unicode) { - isUnicode = unicode; + public Builder setPattern(final String pattern) { + this.pattern = pattern; return this; } /** - * If {@code "true"}, indicates that the column is a character LOB (CLOB). + * If {@code "true"}, indicates that the column is a Unicode String. * * @return this. */ - public Builder setClob(final boolean clob) { - isClob = clob; + public Builder setUnicode(final boolean unicode) { + isUnicode = unicode; return this; } + } - @Override - public ColumnConfig build() { - if (Strings.isEmpty(name)) { - LOGGER.error("The column config is not valid because it does not contain a column name."); - return null; - } + private static final Logger LOGGER = StatusLogger.getLogger(); + /** + * Factory method for creating a column config within the plugin manager. + * + * @see Builder + * @deprecated use {@link #newBuilder()} + */ + @Deprecated + public static ColumnConfig createColumnConfig(final Configuration config, final String name, final String pattern, + final String literalValue, final String eventTimestamp, + final String unicode, final String clob) { + if (Strings.isEmpty(name)) { + LOGGER.error("The column config is not valid because it does not contain a column name."); + return null; + } - final boolean isPattern = Strings.isNotEmpty(pattern); - final boolean isLiteralValue = Strings.isNotEmpty(literal); + final boolean isEventTimestamp = Boolean.parseBoolean(eventTimestamp); + final boolean isUnicode = Booleans.parseBoolean(unicode, true); + final boolean isClob = Boolean.parseBoolean(clob); - if ((isPattern && isLiteralValue) || (isPattern && isEventTimestamp) || (isLiteralValue && isEventTimestamp)) { - LOGGER.error("The pattern, literal, and isEventTimestamp attributes are mutually exclusive."); - return null; - } + return newBuilder() + .setConfiguration(config) + .setName(name) + .setPattern(pattern) + .setLiteral(literalValue) + .setEventTimestamp(isEventTimestamp) + .setUnicode(isUnicode) + .setClob(isClob) + .build(); + } + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + private final String columnName; + private final String columnNameKey; + private final PatternLayout layout; + private final String literalValue; - if (isEventTimestamp) { - return new ColumnConfig(name, null, null, true, false, false); - } + private final boolean eventTimestamp; - if (isLiteralValue) { - return new ColumnConfig(name, null, literal, false, false, false); - } + private final boolean unicode; - if (isPattern) { - final PatternLayout layout = - PatternLayout.newBuilder() - .withPattern(pattern) - .withConfiguration(configuration) - .withAlwaysWriteExceptions(false) - .build(); - return new ColumnConfig(name, layout, null, false, isUnicode, isClob); - } + private final boolean clob; - LOGGER.error("To configure a column you must specify a pattern or literal or set isEventDate to true."); - return null; - } + private ColumnConfig(final String columnName, final PatternLayout layout, final String literalValue, + final boolean eventDate, final boolean unicode, final boolean clob) { + this.columnName = columnName; + this.columnNameKey = ColumnMapping.toKey(columnName); + this.layout = layout; + this.literalValue = literalValue; + this.eventTimestamp = eventDate; + this.unicode = unicode; + this.clob = clob; + } + + public String getColumnName() { + return this.columnName; + } + + public String getColumnNameKey() { + return this.columnNameKey; + } + + public PatternLayout getLayout() { + return this.layout; + } + + public String getLiteralValue() { + return this.literalValue; + } + + public boolean isClob() { + return this.clob; + } + + public boolean isEventTimestamp() { + return this.eventTimestamp; + } + + public boolean isUnicode() { + return this.unicode; + } + + @Override + public String toString() { + return "{ name=" + this.columnName + ", layout=" + this.layout + ", literal=" + this.literalValue + + ", timestamp=" + this.eventTimestamp + " }"; } } http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java index ff801e0..7a29dc7 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppender.java @@ -62,7 +62,7 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan @PluginBuilderAttribute private boolean immediateFail; - + @PluginBuilderAttribute private int bufferSize; @@ -76,6 +76,9 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan @PluginElement("ColumnMappings") private ColumnMapping[] columnMappings; + @PluginBuilderAttribute + private boolean truncateStrings = true; + // TODO Consider moving up to AbstractDatabaseAppender.Builder. @PluginBuilderAttribute private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL_MILLIS; @@ -90,7 +93,8 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan + tableName + ", columnConfigs=" + Arrays.toString(columnConfigs) + ", columnMappings=" + Arrays.toString(columnMappings) + '}'; final JdbcDatabaseManager manager = JdbcDatabaseManager.getManager(managerName, bufferSize, getLayout(), - connectionSource, tableName, columnConfigs, columnMappings, immediateFail, reconnectIntervalMillis); + connectionSource, tableName, columnConfigs, columnMappings, immediateFail, reconnectIntervalMillis, + truncateStrings); if (manager == null) { return null; } @@ -109,8 +113,8 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan /** * If an integer greater than 0, this causes the appender to buffer log events and flush whenever the buffer * reaches this size. - * - * @param bufferSize buffer size. + * + * @param bufferSize buffer size. * * @return this */ @@ -121,8 +125,8 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan /** * Information about the columns that log event data should be inserted into and how to insert that data. - * - * @param columnConfigs Column configurations. + * + * @param columnConfigs Column configurations. * * @return this */ @@ -138,7 +142,7 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan /** * The connections source from which database connections should be retrieved. - * + * * @param connectionSource The connections source. * * @return this @@ -148,18 +152,18 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan return asBuilder(); } - public void setImmediateFail(boolean immediateFail) { + public void setImmediateFail(final boolean immediateFail) { this.immediateFail = immediateFail; } - public void setReconnectIntervalMillis(long reconnectIntervalMillis) { + public void setReconnectIntervalMillis(final long reconnectIntervalMillis) { this.reconnectIntervalMillis = reconnectIntervalMillis; } /** * The name of the database table to insert log events into. - * - * @param tableName The database table name. + * + * @param tableName The database table name. * * @return this */ @@ -168,6 +172,11 @@ public final class JdbcAppender extends AbstractDatabaseAppender<JdbcDatabaseMan return asBuilder(); } + public B setTruncateStrings(final boolean truncateStrings) { + this.truncateStrings = truncateStrings; + return asBuilder(); + } + } /** http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java index 26130b4..b449272 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcDatabaseManager.java @@ -23,13 +23,16 @@ import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.NClob; import java.sql.PreparedStatement; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.CountDownLatch; @@ -69,10 +72,12 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { private final boolean immediateFail; private final boolean retry; private final long reconnectIntervalMillis; + private final boolean truncateStrings; protected FactoryData(final int bufferSize, final Layout<? extends Serializable> layout, final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs, - final ColumnMapping[] columnMappings, final boolean immediateFail, final long reconnectIntervalMillis) { + final ColumnMapping[] columnMappings, final boolean immediateFail, final long reconnectIntervalMillis, + final boolean truncateStrings) { super(bufferSize, layout); this.connectionSource = connectionSource; this.tableName = tableName; @@ -81,14 +86,15 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { this.immediateFail = immediateFail; this.retry = reconnectIntervalMillis > 0; this.reconnectIntervalMillis = reconnectIntervalMillis; + this.truncateStrings = truncateStrings; } @Override public String toString() { return String.format( - "FactoryData [connectionSource=%s, tableName=%s, columnConfigs=%s, columnMappings=%s, immediateFail=%s, retry=%s, reconnectIntervalMillis=%,d]", + "FactoryData [connectionSource=%s, tableName=%s, columnConfigs=%s, columnMappings=%s, immediateFail=%s, retry=%s, reconnectIntervalMillis=%s, truncateStrings=%s]", connectionSource, tableName, Arrays.toString(columnConfigs), Arrays.toString(columnMappings), - immediateFail, retry, reconnectIntervalMillis); + immediateFail, retry, reconnectIntervalMillis, truncateStrings); } } @@ -101,25 +107,14 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { @Override public JdbcDatabaseManager createManager(final String name, final FactoryData data) { - final StringBuilder sb = new StringBuilder("INSERT INTO ").append(data.tableName).append(" ("); + final StringBuilder sb = new StringBuilder("insert into ").append(data.tableName).append(" ("); // so this gets a little more complicated now that there are two ways to configure column mappings, but // both mappings follow the same exact pattern for the prepared statement + appendColumnNames("INSERT", data, sb); + sb.append(") values ("); int i = 1; for (final ColumnMapping mapping : data.columnMappings) { final String mappingName = mapping.getName(); - logger().trace("Adding INSERT ColumnMapping[{}]: {}={} ", i++, mappingName, mapping); - sb.append(mappingName).append(','); - } - for (final ColumnConfig config : data.columnConfigs) { - sb.append(config.getColumnName()).append(','); - } - // at least one of those arrays is guaranteed to be non-empty - sb.setCharAt(sb.length() - 1, ')'); - sb.append(" VALUES ("); - i = 1; - final List<ColumnMapping> columnMappings = new ArrayList<>(data.columnMappings.length); - for (final ColumnMapping mapping : data.columnMappings) { - final String mappingName = mapping.getName(); if (Strings.isNotEmpty(mapping.getLiteralValue())) { logger().trace("Adding INSERT VALUES literal for ColumnMapping[{}]: {}={} ", i, mappingName, mapping.getLiteralValue()); @@ -128,12 +123,10 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { logger().trace("Adding INSERT VALUES parameter for ColumnMapping[{}]: {}={} ", i, mappingName, mapping.getParameter()); sb.append(mapping.getParameter()); - columnMappings.add(mapping); } else { logger().trace("Adding INSERT VALUES parameter marker for ColumnMapping[{}]: {}={} ", i, mappingName, PARAMETER_MARKER); sb.append(PARAMETER_MARKER); - columnMappings.add(mapping); } sb.append(','); i++; @@ -159,7 +152,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { /** * Handles reconnecting to JDBC once on a Thread. */ - private class Reconnector extends Log4jThread { + private final class Reconnector extends Log4jThread { private final CountDownLatch latch = new CountDownLatch(1); private volatile boolean shutdown = false; @@ -205,8 +198,161 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { } + private static final class ResultSetColumnMetaData { + + private final String schemaName; + private final String catalogName; + private final String tableName; + private final String name; + private final String nameKey; + private final String label; + private final int displaySize; + private final int type; + private final String typeName; + private final String className; + private final int precision; + private final int scale; + private final boolean isStringType; + + public ResultSetColumnMetaData(final ResultSetMetaData rsMetaData, final int j) throws SQLException { + // @formatter:off + this(rsMetaData.getSchemaName(j), + rsMetaData.getCatalogName(j), + rsMetaData.getTableName(j), + rsMetaData.getColumnName(j), + rsMetaData.getColumnLabel(j), + rsMetaData.getColumnDisplaySize(j), + rsMetaData.getColumnType(j), + rsMetaData.getColumnTypeName(j), + rsMetaData.getColumnClassName(j), + rsMetaData.getPrecision(j), + rsMetaData.getScale(j)); + // @formatter:on + } + + private ResultSetColumnMetaData(final String schemaName, final String catalogName, final String tableName, + final String name, final String label, final int displaySize, final int type, final String typeName, + final String className, final int precision, final int scale) { + super(); + this.schemaName = schemaName; + this.catalogName = catalogName; + this.tableName = tableName; + this.name = name; + this.nameKey = ColumnMapping.toKey(name); + this.label = label; + this.displaySize = displaySize; + this.type = type; + this.typeName = typeName; + this.className = className; + this.precision = precision; + this.scale = scale; + // TODO How about also using the className? + // @formatter:off + this.isStringType = + type == Types.CHAR || + type == Types.LONGNVARCHAR || + type == Types.LONGVARCHAR || + type == Types.NVARCHAR || + type == Types.VARCHAR; + // @formatter:on + } + + public String getCatalogName() { + return catalogName; + } + + public String getClassName() { + return className; + } + + public int getDisplaySize() { + return displaySize; + } + + public String getLabel() { + return label; + } + + public String getName() { + return name; + } + + public String getNameKey() { + return nameKey; + } + + public int getPrecision() { + return precision; + } + + public int getScale() { + return scale; + } + + public String getSchemaName() { + return schemaName; + } + + public String getTableName() { + return tableName; + } + + public int getType() { + return type; + } + + public String getTypeName() { + return typeName; + } + + public boolean isStringType() { + return this.isStringType; + } + + @Override + public String toString() { + return String.format( + "ColumnMetaData [schemaName=%s, catalogName=%s, tableName=%s, name=%s, nameKey=%s, label=%s, displaySize=%s, type=%s, typeName=%s, className=%s, precision=%s, scale=%s, isStringType=%s]", + schemaName, catalogName, tableName, name, nameKey, label, displaySize, type, typeName, className, + precision, scale, isStringType); + } + + public String truncate(final String string) { + return precision > 0 ? Strings.left(string, precision) : string; + } + } + private static final JdbcDatabaseManagerFactory INSTANCE = new JdbcDatabaseManagerFactory(); + private static void appendColumnName(final int i, final String columnName, final StringBuilder sb) { + if (i > 1) { + sb.append(','); + } + sb.append(columnName); + } + + /** + * Appends column names to the given buffer in the format {@code "A,B,C"}. + */ + private static void appendColumnNames(final String sqlVerb, final FactoryData data, final StringBuilder sb) { + // so this gets a little more complicated now that there are two ways to configure column mappings, but + // both mappings follow the same exact pattern for the prepared statement + int i = 1; + final String messagePattern = "Appending {} {}[{}]: {}={} "; + for (final ColumnMapping colMapping : data.columnMappings) { + final String columnName = colMapping.getName(); + appendColumnName(i, columnName, sb); + logger().trace(messagePattern, sqlVerb, colMapping.getClass().getSimpleName(), i, columnName, colMapping); + i++; + } + for (final ColumnConfig colConfig : data.columnConfigs) { + final String columnName = colConfig.getColumnName(); + appendColumnName(i, columnName, sb); + logger().trace(messagePattern, sqlVerb, colConfig.getClass().getSimpleName(), i, columnName, colConfig); + i++; + } + } + private static JdbcDatabaseManagerFactory getFactory() { return INSTANCE; } @@ -228,7 +374,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs) { return getManager( name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs, - new ColumnMapping[0], false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS), + new ColumnMapping[0], false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory()); } @@ -237,20 +383,19 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { * * @param name The name of the manager, which should include connection details and hashed passwords where possible. * @param bufferSize The size of the log event buffer. + * @param layout The Appender-level layout * @param connectionSource The source for connections to the database. * @param tableName The name of the database table to insert log events into. * @param columnConfigs Configuration information about the log table columns. * @param columnMappings column mapping configuration (including type conversion). * @return a new or existing JDBC manager as applicable. - * @deprecated use - * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)} */ @Deprecated public static JdbcDatabaseManager getManager(final String name, final int bufferSize, - final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs, - final ColumnMapping[] columnMappings) { - return getManager(name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs, - columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS), getFactory()); + final Layout<? extends Serializable> layout, final ConnectionSource connectionSource, + final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings) { + return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs, + columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory()); } /** @@ -258,19 +403,24 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { * * @param name The name of the manager, which should include connection details and hashed passwords where possible. * @param bufferSize The size of the log event buffer. - * @param layout The Appender-level layout + * @param layout * @param connectionSource The source for connections to the database. * @param tableName The name of the database table to insert log events into. * @param columnConfigs Configuration information about the log table columns. * @param columnMappings column mapping configuration (including type conversion). + * @param reconnectIntervalMillis + * @param immediateFail * @return a new or existing JDBC manager as applicable. + * @deprecated use + * {@link #getManager(String, int, Layout, ConnectionSource, String, ColumnConfig[], ColumnMapping[], boolean, long)} */ @Deprecated public static JdbcDatabaseManager getManager(final String name, final int bufferSize, final Layout<? extends Serializable> layout, final ConnectionSource connectionSource, - final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings) { - return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs, - columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS), getFactory()); + final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings, + final boolean immediateFail, final long reconnectIntervalMillis) { + return getManager(name, new FactoryData(bufferSize, null, connectionSource, tableName, columnConfigs, + columnMappings, false, AbstractDatabaseAppender.DEFAULT_RECONNECT_INTERVAL_MILLIS, true), getFactory()); } /** @@ -286,14 +436,15 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { * @param immediateFail Whether or not to fail immediately with a {@link AppenderLoggingException} when connecting * to JDBC fails. * @param reconnectIntervalMillis How often to reconnect to the database when a SQL exception is detected. + * @param truncateStrings Whether or not to truncate strings to match column metadata. * @return a new or existing JDBC manager as applicable. */ public static JdbcDatabaseManager getManager(final String name, final int bufferSize, final Layout<? extends Serializable> layout, final ConnectionSource connectionSource, final String tableName, final ColumnConfig[] columnConfigs, final ColumnMapping[] columnMappings, - final boolean immediateFail, final long reconnectIntervalMillis) { + final boolean immediateFail, final long reconnectIntervalMillis, final boolean truncateStrings) { return getManager(name, new FactoryData(bufferSize, layout, connectionSource, tableName, columnConfigs, - columnMappings, immediateFail, reconnectIntervalMillis), getFactory()); + columnMappings, immediateFail, reconnectIntervalMillis, truncateStrings), getFactory()); } // NOTE: prepared statements are prepared in this order: column mappings, then column configs @@ -304,6 +455,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { private volatile PreparedStatement statement; private volatile Reconnector reconnector; private volatile boolean isBatchSupported; + private volatile Map<String, ResultSetColumnMetaData> columnMetaData; private JdbcDatabaseManager(final String name, final String sqlStatement, final List<ColumnConfig> columnConfigs, final FactoryData factoryData) { @@ -344,7 +496,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { } } - protected void closeResources(boolean logExceptions) { + protected void closeResources(final boolean logExceptions) { try { // Closing a statement returns it to the pool when using Apache Commons DBCP. // Closing an already closed statement has no effect. @@ -372,7 +524,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { @Override protected boolean commitAndClose() { - boolean closed = true; + final boolean closed = true; try { if (this.connection != null && !this.connection.isClosed()) { if (this.isBatchSupported && this.statement != null) { @@ -396,7 +548,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { try { this.commitAndClose(); return true; - } catch (AppenderLoggingException e) { + } catch (final AppenderLoggingException e) { // Database connection has likely gone stale. final Throwable cause = e.getCause(); final Throwable actual = cause == null ? e : cause; @@ -415,14 +567,17 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { this.connection = getConnectionSource().getConnection(); logger().debug("Acquired JDBC connection {}", this.connection); logger().debug("Getting connection metadata {}", this.connection); - final DatabaseMetaData metaData = this.connection.getMetaData(); - logger().debug("Connection metadata {}", metaData); - this.isBatchSupported = metaData.supportsBatchUpdates(); + final DatabaseMetaData databaseMetaData = this.connection.getMetaData(); + logger().debug("Connection metadata {}", databaseMetaData); + this.isBatchSupported = databaseMetaData.supportsBatchUpdates(); logger().debug("Connection supportsBatchUpdates: {}", this.isBatchSupported); this.connection.setAutoCommit(false); logger().debug("Preparing SQL {}", this.sqlStatement); this.statement = this.connection.prepareStatement(this.sqlStatement); logger().debug("Prepared SQL {}", this.statement); + if (this.factoryData.truncateStrings) { + initColumnMetaData(); + } } @Override @@ -444,6 +599,15 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { return recon; } + private String createSqlSelect() { + final StringBuilder sb = new StringBuilder("select "); + appendColumnNames("SELECT", this.factoryData, sb); + sb.append(" from "); + sb.append(this.factoryData.tableName); + sb.append(" where 1=0"); + return sb.toString(); + } + public ConnectionSource getConnectionSource() { return factoryData.connectionSource; } @@ -456,6 +620,30 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { return factoryData.tableName; } + private void initColumnMetaData() throws SQLException { + // Could use: + // this.connection.getMetaData().getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern); + // But this returns more data than we need for now, so do a SQL SELECT with 0 result rows instead. + final String sqlSelect = createSqlSelect(); + logger().debug("Getting SQL metadata for table {}: {}", this.factoryData.tableName, sqlSelect); + try (final PreparedStatement mdStatement = this.connection.prepareStatement(sqlSelect)) { + final ResultSetMetaData rsMetaData = mdStatement.getMetaData(); + logger().debug("SQL metadata: {}", rsMetaData); + if (rsMetaData != null) { + final int columnCount = rsMetaData.getColumnCount(); + columnMetaData = new HashMap<>(columnCount); + for (int i = 0, j = 1; i < columnCount; i++, j++) { + final ResultSetColumnMetaData value = new ResultSetColumnMetaData(rsMetaData, j); + columnMetaData.put(value.getNameKey(), value); + } + } else { + logger().warn( + "{}: truncateStrings is true and ResultSetMetaData is null for statement: {}; manager will not perform truncation.", + getClass().getSimpleName(), mdStatement); + } + } + } + private void reconnectOn(final Exception exception) { if (!factoryData.retry) { throw new AppenderLoggingException("Cannot connect and prepare", exception); @@ -480,7 +668,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { private void setFields(final MapMessage<?, ?> mapMessage) throws SQLException { final IndexedReadOnlyStringMap map = mapMessage.getIndexedReadOnlyStringMap(); final String simpleName = statement.getClass().getName(); - int i = 1; // JDBC indices start at 1 + int j = 1; // JDBC indices start at 1 for (final ColumnMapping mapping : this.factoryData.columnMappings) { if (mapping.getLiteralValue() == null) { final String source = mapping.getSource(); @@ -489,14 +677,22 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { if (logger().isTraceEnabled()) { final String valueStr = value instanceof String ? "\"" + value + "\"" : Objects.toString(value, null); - logger().trace("{} setObject({}, {}) for key '{}' and mapping '{}'", simpleName, i, valueStr, key, + logger().trace("{} setObject({}, {}) for key '{}' and mapping '{}'", simpleName, j, valueStr, key, mapping.getName()); } - statement.setObject(i++, value); + setStatementObject(j, mapping.getNameKey(), value); + j++; } } } + /** + * Sets the given Object in the prepared statement. The value is truncated if needed. + */ + private void setStatementObject(final int j, final String nameKey, final Object value) throws SQLException { + statement.setObject(j, truncate(nameKey, value)); + } + @Override protected boolean shutdownInternal() { if (reconnector != null) { @@ -512,6 +708,23 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { // empty } + /** + * Truncates the value if needed. + */ + private Object truncate(final String nameKey, Object value) { + if (value != null && this.factoryData.truncateStrings && columnMetaData != null) { + final ResultSetColumnMetaData resultSetColumnMetaData = columnMetaData.get(nameKey); + if (resultSetColumnMetaData != null) { + if (resultSetColumnMetaData.isStringType()) { + value = resultSetColumnMetaData.truncate(value.toString()); + } + } else { + logger().error("Missing ResultSetColumnMetaData for {}", nameKey); + } + } + return value; + } + @Override protected void writeInternal(final LogEvent event, final Serializable serializable) { StringReader reader = null; @@ -526,30 +739,31 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { if (serializable instanceof MapMessage) { setFields((MapMessage<?, ?>) serializable); } - int i = 1; // JDBC indices start at 1 + int j = 1; // JDBC indices start at 1 for (final ColumnMapping mapping : this.factoryData.columnMappings) { if (ThreadContextMap.class.isAssignableFrom(mapping.getType()) || ReadOnlyStringMap.class.isAssignableFrom(mapping.getType())) { - this.statement.setObject(i++, event.getContextData().toMap()); + this.statement.setObject(j++, event.getContextData().toMap()); } else if (ThreadContextStack.class.isAssignableFrom(mapping.getType())) { - this.statement.setObject(i++, event.getContextStack().asList()); + this.statement.setObject(j++, event.getContextStack().asList()); } else if (Date.class.isAssignableFrom(mapping.getType())) { - this.statement.setObject(i++, DateTypeConverter.fromMillis(event.getTimeMillis(), + this.statement.setObject(j++, DateTypeConverter.fromMillis(event.getTimeMillis(), mapping.getType().asSubclass(Date.class))); } else { final StringLayout layout = mapping.getLayout(); if (layout != null) { if (Clob.class.isAssignableFrom(mapping.getType())) { - this.statement.setClob(i++, new StringReader(layout.toSerializable(event))); + this.statement.setClob(j++, new StringReader(layout.toSerializable(event))); } else if (NClob.class.isAssignableFrom(mapping.getType())) { - this.statement.setNClob(i++, new StringReader(layout.toSerializable(event))); + this.statement.setNClob(j++, new StringReader(layout.toSerializable(event))); } else { final Object value = TypeConverters.convert(layout.toSerializable(event), mapping.getType(), null); if (value == null) { - this.statement.setNull(i++, Types.NULL); + // TODO We might need to always initialize the columnMetaData to specify the type. + this.statement.setNull(j++, Types.NULL); } else { - this.statement.setObject(i++, value); + setStatementObject(j++, mapping.getNameKey(), value); } } } @@ -557,18 +771,20 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { } for (final ColumnConfig column : this.columnConfigs) { if (column.isEventTimestamp()) { - this.statement.setTimestamp(i++, new Timestamp(event.getTimeMillis())); + this.statement.setTimestamp(j++, new Timestamp(event.getTimeMillis())); } else if (column.isClob()) { reader = new StringReader(column.getLayout().toSerializable(event)); if (column.isUnicode()) { - this.statement.setNClob(i++, reader); + this.statement.setNClob(j++, reader); } else { - this.statement.setClob(i++, reader); + this.statement.setClob(j++, reader); } } else if (column.isUnicode()) { - this.statement.setNString(i++, column.getLayout().toSerializable(event)); + this.statement.setNString(j++, Objects.toString( + truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null)); } else { - this.statement.setString(i++, column.getLayout().toSerializable(event)); + this.statement.setString(j++, Objects.toString( + truncate(column.getColumnNameKey(), column.getLayout().toSerializable(event)), null)); } } @@ -601,7 +817,7 @@ public final class JdbcDatabaseManager extends AbstractDatabaseManager { } finally { this.commitAndClose(); } - } catch (DbAppenderLoggingException e) { + } catch (final DbAppenderLoggingException e) { reconnectOn(e); try { this.writeInternal(event, serializable); http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderFactoryMethodTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderFactoryMethodTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderFactoryMethodTest.java index 72f7f39..de61e17 100644 --- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderFactoryMethodTest.java +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/AbstractJdbcAppenderFactoryMethodTest.java @@ -16,6 +16,10 @@ */ package org.apache.logging.log4j.core.appender.db.jdbc; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.sql.Connection; @@ -23,6 +27,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext; @@ -34,10 +39,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - /** * Abstract unit test for JdbcAppender using a {@link FactoryMethodConnectionSource} configuration. */ @@ -110,4 +111,12 @@ public abstract class AbstractJdbcAppenderFactoryMethodTest { } } + @Test + public void testTruncate() { + final Logger logger = LogManager.getLogger(this.getClass().getName() + ".testFactoryMethodConfig"); + // Some drivers and database will not allow more data than the column defines. + // We really need a MySQL databases with a default configuration to test this. + logger.debug(StringUtils.repeat('A', 1000)); + } + } http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppenderMapMessageDataSourceTest.java ---------------------------------------------------------------------- diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppenderMapMessageDataSourceTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppenderMapMessageDataSourceTest.java index 027919d..27a47b7 100644 --- a/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppenderMapMessageDataSourceTest.java +++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/appender/db/jdbc/JdbcAppenderMapMessageDataSourceTest.java @@ -30,6 +30,7 @@ import java.sql.Statement; import javax.sql.DataSource; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.util.Throwables; @@ -97,7 +98,7 @@ public class JdbcAppenderMapMessageDataSourceTest { writer.close(); final Logger logger = LogManager.getLogger(this.getClass().getName() + ".testDataSourceConfig"); - final MapMessage mapMessage = new MapMessage(); + final MapMessage<?, ?> mapMessage = new MapMessage<>(); mapMessage.with("Id", 1); mapMessage.with("ColumnA", "ValueA"); mapMessage.with("ColumnB", "ValueB"); @@ -115,4 +116,28 @@ public class JdbcAppenderMapMessageDataSourceTest { } } } + + @Test + public void testTruncate() throws SQLException { + try (final Connection connection = jdbcRule.getConnectionSource().getConnection()) { + final Logger logger = LogManager.getLogger(this.getClass().getName() + ".testFactoryMethodConfig"); + // Some drivers and database will not allow more data than the column defines. + // We really need a MySQL databases with a default configuration to test this. + final MapMessage<?, ?> mapMessage = new MapMessage<>(); + mapMessage.with("Id", 1); + mapMessage.with("ColumnA", StringUtils.repeat('A', 1000)); + mapMessage.with("ColumnB", StringUtils.repeat('B', 1000)); + logger.info(mapMessage); + try (final Statement statement = connection.createStatement(); + final ResultSet resultSet = statement + .executeQuery("SELECT Id, ColumnA, ColumnB FROM dsLogEntry ORDER BY Id")) { + + assertTrue("There should be at least one row.", resultSet.next()); + + Assert.assertEquals(1, resultSet.getInt("Id")); + + assertFalse("There should not be two rows.", resultSet.next()); + } + } + } } http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/log4j-jdbc-dbcp2/src/test/resources/log4j2-jdbc-dbcp2.xml ---------------------------------------------------------------------- diff --git a/log4j-jdbc-dbcp2/src/test/resources/log4j2-jdbc-dbcp2.xml b/log4j-jdbc-dbcp2/src/test/resources/log4j2-jdbc-dbcp2.xml index 4c8aadb..7821fd0 100644 --- a/log4j-jdbc-dbcp2/src/test/resources/log4j2-jdbc-dbcp2.xml +++ b/log4j-jdbc-dbcp2/src/test/resources/log4j2-jdbc-dbcp2.xml @@ -39,6 +39,10 @@ rollbackOnReturn="true" validationQuery="" validationQueryTimeoutSeconds="-1"> + <DisconnectionSqlCodes> + <String>1</String> + <String>2</String> + </DisconnectionSqlCodes> </PoolableConnectionFactory> </PoolingDriver> <ColumnMapping name="ColumnA" /> http://git-wip-us.apache.org/repos/asf/logging-log4j2/blob/0cd480ff/src/changes/changes.xml ---------------------------------------------------------------------- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f641644..2c5bcb8 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -130,6 +130,9 @@ <action issue="LOG4J2-2508" dev="ggregory" type="fix"> JDBC Appender fails when using both parameter, source, and literal ColumnMapping elements. </action> + <action issue="LOG4J2-2509" dev="ggregory" type="add"> + Allow a JDBC Appender to truncate strings to match a table's metadata column length limit. + </action> </release> <release version="2.11.1" date="2018-07-22" description="GA Release 2.11.1"> <action issue="LOG4J2-2389" dev="rgoers" type="fix" due-to="Liu Wen">