001/**
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v1.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.classic.db;
015
016import static ch.qos.logback.core.db.DBHelper.closeStatement;
017
018import java.lang.reflect.Method;
019import java.sql.Connection;
020import java.sql.PreparedStatement;
021import java.sql.SQLException;
022import java.util.HashMap;
023import java.util.Map;
024import java.util.Set;
025
026import ch.qos.logback.classic.db.names.DBNameResolver;
027import ch.qos.logback.classic.db.names.DefaultDBNameResolver;
028import ch.qos.logback.classic.spi.*;
029import ch.qos.logback.core.CoreConstants;
030import ch.qos.logback.core.db.DBAppenderBase;
031
032/**
033 * The DBAppender inserts logging events into three database tables in a format
034 * independent of the Java programming language.
035 *
036 * For more information about this appender, please refer to the online manual
037 * at http://logback.qos.ch/manual/appenders.html#DBAppender
038 *
039 * @author Ceki Gülcü
040 * @author Ray DeCampo
041 * @author Sébastien Pennec
042 */
043public class DBAppender extends DBAppenderBase<ILoggingEvent> {
044    protected String insertPropertiesSQL;
045    protected String insertExceptionSQL;
046    protected String insertSQL;
047    protected static final Method GET_GENERATED_KEYS_METHOD;
048
049    private DBNameResolver dbNameResolver;
050
051    static final int TIMESTMP_INDEX = 1;
052    static final int FORMATTED_MESSAGE_INDEX = 2;
053    static final int LOGGER_NAME_INDEX = 3;
054    static final int LEVEL_STRING_INDEX = 4;
055    static final int THREAD_NAME_INDEX = 5;
056    static final int REFERENCE_FLAG_INDEX = 6;
057    static final int ARG0_INDEX = 7;
058    static final int ARG1_INDEX = 8;
059    static final int ARG2_INDEX = 9;
060    static final int ARG3_INDEX = 10;
061    static final int CALLER_FILENAME_INDEX = 11;
062    static final int CALLER_CLASS_INDEX = 12;
063    static final int CALLER_METHOD_INDEX = 13;
064    static final int CALLER_LINE_INDEX = 14;
065    static final int EVENT_ID_INDEX = 15;
066
067    static final StackTraceElement EMPTY_CALLER_DATA = CallerData.naInstance();
068
069    static {
070        // PreparedStatement.getGeneratedKeys() method was added in JDK 1.4
071        Method getGeneratedKeysMethod;
072        try {
073            // the
074            getGeneratedKeysMethod = PreparedStatement.class.getMethod("getGeneratedKeys", (Class[]) null);
075        } catch (Exception ex) {
076            getGeneratedKeysMethod = null;
077        }
078        GET_GENERATED_KEYS_METHOD = getGeneratedKeysMethod;
079    }
080
081    public void setDbNameResolver(DBNameResolver dbNameResolver) {
082        this.dbNameResolver = dbNameResolver;
083    }
084
085    @Override
086    public void start() {
087        if (dbNameResolver == null)
088            dbNameResolver = new DefaultDBNameResolver();
089        insertExceptionSQL = SQLBuilder.buildInsertExceptionSQL(dbNameResolver);
090        insertPropertiesSQL = SQLBuilder.buildInsertPropertiesSQL(dbNameResolver);
091        insertSQL = SQLBuilder.buildInsertSQL(dbNameResolver);
092        super.start();
093    }
094
095    @Override
096    protected void subAppend(ILoggingEvent event, Connection connection, PreparedStatement insertStatement) throws Throwable {
097
098        bindLoggingEventWithInsertStatement(insertStatement, event);
099        bindLoggingEventArgumentsWithPreparedStatement(insertStatement, event.getArgumentArray());
100
101        // This is expensive... should we do it every time?
102        bindCallerDataWithPreparedStatement(insertStatement, event.getCallerData());
103
104        int updateCount = insertStatement.executeUpdate();
105        if (updateCount != 1) {
106            addWarn("Failed to insert loggingEvent");
107        }
108    }
109
110    protected void secondarySubAppend(ILoggingEvent event, Connection connection, long eventId) throws Throwable {
111        Map<String, String> mergedMap = mergePropertyMaps(event);
112        insertProperties(mergedMap, connection, eventId);
113
114        if (event.getThrowableProxy() != null) {
115            insertThrowable(event.getThrowableProxy(), connection, eventId);
116        }
117    }
118
119    void bindLoggingEventWithInsertStatement(PreparedStatement stmt, ILoggingEvent event) throws SQLException {
120        stmt.setLong(TIMESTMP_INDEX, event.getTimeStamp());
121        stmt.setString(FORMATTED_MESSAGE_INDEX, event.getFormattedMessage());
122        stmt.setString(LOGGER_NAME_INDEX, event.getLoggerName());
123        stmt.setString(LEVEL_STRING_INDEX, event.getLevel().toString());
124        stmt.setString(THREAD_NAME_INDEX, event.getThreadName());
125        stmt.setShort(REFERENCE_FLAG_INDEX, DBHelper.computeReferenceMask(event));
126    }
127
128    void bindLoggingEventArgumentsWithPreparedStatement(PreparedStatement stmt, Object[] argArray) throws SQLException {
129
130        int arrayLen = argArray != null ? argArray.length : 0;
131
132        for (int i = 0; i < arrayLen && i < 4; i++) {
133            stmt.setString(ARG0_INDEX + i, asStringTruncatedTo254(argArray[i]));
134        }
135        if (arrayLen < 4) {
136            for (int i = arrayLen; i < 4; i++) {
137                stmt.setString(ARG0_INDEX + i, null);
138            }
139        }
140    }
141
142    String asStringTruncatedTo254(Object o) {
143        String s = null;
144        if (o != null) {
145            s = o.toString();
146        }
147
148        if (s == null) {
149            return null;
150        }
151        if (s.length() <= 254) {
152            return s;
153        } else {
154            return s.substring(0, 254);
155        }
156    }
157
158    void bindCallerDataWithPreparedStatement(PreparedStatement stmt, StackTraceElement[] callerDataArray) throws SQLException {
159
160        StackTraceElement caller = extractFirstCaller(callerDataArray);
161
162        stmt.setString(CALLER_FILENAME_INDEX, caller.getFileName());
163        stmt.setString(CALLER_CLASS_INDEX, caller.getClassName());
164        stmt.setString(CALLER_METHOD_INDEX, caller.getMethodName());
165        stmt.setString(CALLER_LINE_INDEX, Integer.toString(caller.getLineNumber()));
166    }
167
168    private StackTraceElement extractFirstCaller(StackTraceElement[] callerDataArray) {
169        StackTraceElement caller = EMPTY_CALLER_DATA;
170        if (hasAtLeastOneNonNullElement(callerDataArray))
171            caller = callerDataArray[0];
172        return caller;
173    }
174
175    private boolean hasAtLeastOneNonNullElement(StackTraceElement[] callerDataArray) {
176        return callerDataArray != null && callerDataArray.length > 0 && callerDataArray[0] != null;
177    }
178
179    Map<String, String> mergePropertyMaps(ILoggingEvent event) {
180        Map<String, String> mergedMap = new HashMap<String, String>();
181        // we add the context properties first, then the event properties, since
182        // we consider that event-specific properties should have priority over
183        // context-wide properties.
184        Map<String, String> loggerContextMap = event.getLoggerContextVO().getPropertyMap();
185        Map<String, String> mdcMap = event.getMDCPropertyMap();
186        if (loggerContextMap != null) {
187            mergedMap.putAll(loggerContextMap);
188        }
189        if (mdcMap != null) {
190            mergedMap.putAll(mdcMap);
191        }
192
193        return mergedMap;
194    }
195
196    @Override
197    protected Method getGeneratedKeysMethod() {
198        return GET_GENERATED_KEYS_METHOD;
199    }
200
201    @Override
202    protected String getInsertSQL() {
203        return insertSQL;
204    }
205
206    protected void insertProperties(Map<String, String> mergedMap, Connection connection, long eventId) throws SQLException {
207        Set<String> propertiesKeys = mergedMap.keySet();
208        if (propertiesKeys.size() > 0) {
209            PreparedStatement insertPropertiesStatement = null;
210            try {
211                insertPropertiesStatement = connection.prepareStatement(insertPropertiesSQL);
212
213                for (String key : propertiesKeys) {
214                    String value = mergedMap.get(key);
215
216                    insertPropertiesStatement.setLong(1, eventId);
217                    insertPropertiesStatement.setString(2, key);
218                    insertPropertiesStatement.setString(3, value);
219
220                    if (cnxSupportsBatchUpdates) {
221                        insertPropertiesStatement.addBatch();
222                    } else {
223                        insertPropertiesStatement.execute();
224                    }
225                }
226
227                if (cnxSupportsBatchUpdates) {
228                    insertPropertiesStatement.executeBatch();
229                }
230            } finally {
231                closeStatement(insertPropertiesStatement);
232            }
233        }
234    }
235
236    /**
237     * Add an exception statement either as a batch or execute immediately if
238     * batch updates are not supported.
239     */
240    void updateExceptionStatement(PreparedStatement exceptionStatement, String txt, short i, long eventId) throws SQLException {
241        exceptionStatement.setLong(1, eventId);
242        exceptionStatement.setShort(2, i);
243        exceptionStatement.setString(3, txt);
244        if (cnxSupportsBatchUpdates) {
245            exceptionStatement.addBatch();
246        } else {
247            exceptionStatement.execute();
248        }
249    }
250
251    short buildExceptionStatement(IThrowableProxy tp, short baseIndex, PreparedStatement insertExceptionStatement, long eventId) throws SQLException {
252
253        StringBuilder buf = new StringBuilder();
254        ThrowableProxyUtil.subjoinFirstLine(buf, tp);
255        updateExceptionStatement(insertExceptionStatement, buf.toString(), baseIndex++, eventId);
256
257        int commonFrames = tp.getCommonFrames();
258        StackTraceElementProxy[] stepArray = tp.getStackTraceElementProxyArray();
259        for (int i = 0; i < stepArray.length - commonFrames; i++) {
260            StringBuilder sb = new StringBuilder();
261            sb.append(CoreConstants.TAB);
262            ThrowableProxyUtil.subjoinSTEP(sb, stepArray[i]);
263            updateExceptionStatement(insertExceptionStatement, sb.toString(), baseIndex++, eventId);
264        }
265
266        if (commonFrames > 0) {
267            StringBuilder sb = new StringBuilder();
268            sb.append(CoreConstants.TAB).append("... ").append(commonFrames).append(" common frames omitted");
269            updateExceptionStatement(insertExceptionStatement, sb.toString(), baseIndex++, eventId);
270        }
271
272        return baseIndex;
273    }
274
275    protected void insertThrowable(IThrowableProxy tp, Connection connection, long eventId) throws SQLException {
276
277        PreparedStatement exceptionStatement = null;
278        try {
279            exceptionStatement = connection.prepareStatement(insertExceptionSQL);
280
281            short baseIndex = 0;
282            while (tp != null) {
283                baseIndex = buildExceptionStatement(tp, baseIndex, exceptionStatement, eventId);
284                tp = tp.getCause();
285            }
286
287            if (cnxSupportsBatchUpdates) {
288                exceptionStatement.executeBatch();
289            }
290        } finally {
291            closeStatement(exceptionStatement);
292        }
293
294    }
295}