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