001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.core.appender.nosql; 018 019import java.io.Serializable; 020 021import org.apache.logging.log4j.Marker; 022import org.apache.logging.log4j.ThreadContext; 023import org.apache.logging.log4j.core.LogEvent; 024import org.apache.logging.log4j.core.appender.AppenderLoggingException; 025import org.apache.logging.log4j.core.appender.ManagerFactory; 026import org.apache.logging.log4j.core.appender.db.AbstractDatabaseManager; 027import org.apache.logging.log4j.core.util.Closer; 028import org.apache.logging.log4j.message.MapMessage; 029import org.apache.logging.log4j.util.ReadOnlyStringMap; 030 031/** 032 * An {@link AbstractDatabaseManager} implementation for all NoSQL databases. 033 * 034 * @param <W> A type parameter for reassuring the compiler that all operations are using the same {@link NoSqlObject}. 035 */ 036public final class NoSqlDatabaseManager<W> extends AbstractDatabaseManager { 037 private static final NoSQLDatabaseManagerFactory FACTORY = new NoSQLDatabaseManagerFactory(); 038 039 private final NoSqlProvider<NoSqlConnection<W, ? extends NoSqlObject<W>>> provider; 040 041 private NoSqlConnection<W, ? extends NoSqlObject<W>> connection; 042 043 private NoSqlDatabaseManager(final String name, final int bufferSize, 044 final NoSqlProvider<NoSqlConnection<W, ? extends NoSqlObject<W>>> provider) { 045 super(name, bufferSize); 046 this.provider = provider; 047 } 048 049 @Override 050 protected void startupInternal() { 051 // nothing to see here 052 } 053 054 @Override 055 protected boolean shutdownInternal() { 056 // NoSQL doesn't use transactions, so all we need to do here is simply close the client 057 return Closer.closeSilently(this.connection); 058 } 059 060 @Override 061 protected void connectAndStart() { 062 try { 063 this.connection = this.provider.getConnection(); 064 } catch (final Exception e) { 065 throw new AppenderLoggingException("Failed to get connection from NoSQL connection provider.", e); 066 } 067 } 068 069 @Override 070 protected void writeInternal(final LogEvent event, final Serializable serializable) { 071 if (!this.isRunning() || this.connection == null || this.connection.isClosed()) { 072 throw new AppenderLoggingException( 073 "Cannot write logging event; NoSQL manager not connected to the database."); 074 } 075 076 final NoSqlObject<W> entity = this.connection.createObject(); 077 if (serializable instanceof MapMessage) { 078 setFields((MapMessage<?, ?>) serializable, entity); 079 } else { 080 setFields(event, entity); 081 } 082 083 this.connection.insertObject(entity); 084 } 085 086 private void setFields(final MapMessage<?, ?> mapMessage, final NoSqlObject<W> noSqlObject) { 087 // Map without calling org.apache.logging.log4j.message.MapMessage#getData() which makes a copy of the map. 088 mapMessage.forEach((key, value) -> noSqlObject.set(key, value)); 089 } 090 091 private void setFields(final LogEvent event, final NoSqlObject<W> entity) { 092 entity.set("level", event.getLevel()); 093 entity.set("loggerName", event.getLoggerName()); 094 entity.set("message", event.getMessage() == null ? null : event.getMessage().getFormattedMessage()); 095 096 final StackTraceElement source = event.getSource(); 097 if (source == null) { 098 entity.set("source", (Object) null); 099 } else { 100 entity.set("source", this.convertStackTraceElement(source)); 101 } 102 103 final Marker marker = event.getMarker(); 104 if (marker == null) { 105 entity.set("marker", (Object) null); 106 } else { 107 entity.set("marker", buildMarkerEntity(marker)); 108 } 109 110 entity.set("threadId", event.getThreadId()); 111 entity.set("threadName", event.getThreadName()); 112 entity.set("threadPriority", event.getThreadPriority()); 113 entity.set("millis", event.getTimeMillis()); 114 entity.set("date", new java.util.Date(event.getTimeMillis())); 115 116 @SuppressWarnings("ThrowableResultOfMethodCallIgnored") 117 Throwable thrown = event.getThrown(); 118 if (thrown == null) { 119 entity.set("thrown", (Object) null); 120 } else { 121 final NoSqlObject<W> originalExceptionEntity = this.connection.createObject(); 122 NoSqlObject<W> exceptionEntity = originalExceptionEntity; 123 exceptionEntity.set("type", thrown.getClass().getName()); 124 exceptionEntity.set("message", thrown.getMessage()); 125 exceptionEntity.set("stackTrace", this.convertStackTrace(thrown.getStackTrace())); 126 while (thrown.getCause() != null) { 127 thrown = thrown.getCause(); 128 final NoSqlObject<W> causingExceptionEntity = this.connection.createObject(); 129 causingExceptionEntity.set("type", thrown.getClass().getName()); 130 causingExceptionEntity.set("message", thrown.getMessage()); 131 causingExceptionEntity.set("stackTrace", this.convertStackTrace(thrown.getStackTrace())); 132 exceptionEntity.set("cause", causingExceptionEntity); 133 exceptionEntity = causingExceptionEntity; 134 } 135 136 entity.set("thrown", originalExceptionEntity); 137 } 138 139 final ReadOnlyStringMap contextMap = event.getContextData(); 140 if (contextMap == null) { 141 entity.set("contextMap", (Object) null); 142 } else { 143 final NoSqlObject<W> contextMapEntity = this.connection.createObject(); 144 contextMap.forEach((key, val) -> contextMapEntity.set(key, val)); 145 entity.set("contextMap", contextMapEntity); 146 } 147 148 final ThreadContext.ContextStack contextStack = event.getContextStack(); 149 if (contextStack == null) { 150 entity.set("contextStack", (Object) null); 151 } else { 152 entity.set("contextStack", contextStack.asList().toArray()); 153 } 154 } 155 156 private NoSqlObject<W> buildMarkerEntity(final Marker marker) { 157 final NoSqlObject<W> entity = this.connection.createObject(); 158 entity.set("name", marker.getName()); 159 160 final Marker[] parents = marker.getParents(); 161 if (parents != null) { 162 @SuppressWarnings("unchecked") 163 final NoSqlObject<W>[] parentEntities = new NoSqlObject[parents.length]; 164 for (int i = 0; i < parents.length; i++) { 165 parentEntities[i] = buildMarkerEntity(parents[i]); 166 } 167 entity.set("parents", parentEntities); 168 } 169 return entity; 170 } 171 172 @Override 173 protected boolean commitAndClose() { 174 // all NoSQL drivers auto-commit (since NoSQL doesn't generally use the concept of transactions). 175 // also, all our NoSQL drivers use internal connection pooling and provide clients, not connections. 176 // thus, we should not be closing the client until shutdown as NoSQL is very different from SQL. 177 // see LOG4J2-591 and LOG4J2-676 178 return true; 179 } 180 181 private NoSqlObject<W>[] convertStackTrace(final StackTraceElement[] stackTrace) { 182 final NoSqlObject<W>[] stackTraceEntities = this.connection.createList(stackTrace.length); 183 for (int i = 0; i < stackTrace.length; i++) { 184 stackTraceEntities[i] = this.convertStackTraceElement(stackTrace[i]); 185 } 186 return stackTraceEntities; 187 } 188 189 private NoSqlObject<W> convertStackTraceElement(final StackTraceElement element) { 190 final NoSqlObject<W> elementEntity = this.connection.createObject(); 191 elementEntity.set("className", element.getClassName()); 192 elementEntity.set("methodName", element.getMethodName()); 193 elementEntity.set("fileName", element.getFileName()); 194 elementEntity.set("lineNumber", element.getLineNumber()); 195 return elementEntity; 196 } 197 198 /** 199 * Creates a NoSQL manager for use within the {@link NoSqlAppender}, or returns a suitable one if it already exists. 200 * 201 * @param name The name of the manager, which should include connection details and hashed passwords where possible. 202 * @param bufferSize The size of the log event buffer. 203 * @param provider A provider instance which will be used to obtain connections to the chosen NoSQL database. 204 * @return a new or existing NoSQL manager as applicable. 205 */ 206 public static NoSqlDatabaseManager<?> getNoSqlDatabaseManager(final String name, final int bufferSize, 207 final NoSqlProvider<?> provider) { 208 return AbstractDatabaseManager.getManager(name, new FactoryData(bufferSize, provider), FACTORY); 209 } 210 211 /** 212 * Encapsulates data that {@link NoSQLDatabaseManagerFactory} uses to create managers. 213 */ 214 private static final class FactoryData extends AbstractDatabaseManager.AbstractFactoryData { 215 private final NoSqlProvider<?> provider; 216 217 protected FactoryData(final int bufferSize, final NoSqlProvider<?> provider) { 218 super(bufferSize, null); 219 this.provider = provider; 220 } 221 } 222 223 /** 224 * Creates managers. 225 */ 226 private static final class NoSQLDatabaseManagerFactory implements 227 ManagerFactory<NoSqlDatabaseManager<?>, FactoryData> { 228 @Override 229 @SuppressWarnings("unchecked") 230 public NoSqlDatabaseManager<?> createManager(final String name, final FactoryData data) { 231 return new NoSqlDatabaseManager(name, data.getBufferSize(), data.provider); 232 } 233 } 234}