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.mongodb;
018
019import java.lang.reflect.Field;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.List;
023
024import org.apache.logging.log4j.Logger;
025import org.apache.logging.log4j.core.Core;
026import org.apache.logging.log4j.core.config.plugins.Plugin;
027import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
028import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
029import org.apache.logging.log4j.core.config.plugins.PluginFactory;
030import org.apache.logging.log4j.core.config.plugins.convert.TypeConverters;
031import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
032import org.apache.logging.log4j.core.config.plugins.validation.constraints.ValidHost;
033import org.apache.logging.log4j.core.config.plugins.validation.constraints.ValidPort;
034import org.apache.logging.log4j.core.filter.AbstractFilterable;
035import org.apache.logging.log4j.core.util.NameUtil;
036import org.apache.logging.log4j.core.appender.nosql.NoSqlProvider;
037import org.apache.logging.log4j.status.StatusLogger;
038import org.apache.logging.log4j.util.LoaderUtil;
039import org.apache.logging.log4j.util.Strings;
040
041import com.mongodb.DB;
042import com.mongodb.MongoClient;
043import com.mongodb.MongoCredential;
044import com.mongodb.ServerAddress;
045import com.mongodb.WriteConcern;
046
047/**
048 * The MongoDB implementation of {@link NoSqlProvider}.
049 */
050@Plugin(name = "MongoDb", category = Core.CATEGORY_NAME, printObject = true)
051public final class MongoDbProvider implements NoSqlProvider<MongoDbConnection> {
052
053    private static final WriteConcern DEFAULT_WRITE_CONCERN = WriteConcern.ACKNOWLEDGED;
054    private static final Logger LOGGER = StatusLogger.getLogger();
055    private static final int DEFAULT_PORT = 27017;
056    private static final int DEFAULT_COLLECTION_SIZE = 536870912;
057
058    private final String collectionName;
059    private final DB database;
060    private final String description;
061    private final WriteConcern writeConcern;
062    private final boolean isCapped;
063    private final Integer collectionSize;
064
065    private MongoDbProvider(final DB database, final WriteConcern writeConcern, final String collectionName,
066            final boolean isCapped, final Integer collectionSize, final String description) {
067        this.database = database;
068        this.writeConcern = writeConcern;
069        this.collectionName = collectionName;
070        this.isCapped = isCapped;
071        this.collectionSize = collectionSize;
072        this.description = "mongoDb{ " + description + " }";
073    }
074
075    @Override
076    public MongoDbConnection getConnection() {
077        return new MongoDbConnection(this.database, this.writeConcern, this.collectionName, this.isCapped, this.collectionSize);
078    }
079
080    @Override
081    public String toString() {
082        return this.description;
083    }
084
085    /**
086     * Factory method for creating a MongoDB provider within the plugin manager.
087     *
088     * @param collectionName The name of the MongoDB collection to which log events should be written.
089     * @param writeConcernConstant The {@link WriteConcern} constant to control writing details, defaults to
090     *                             {@link WriteConcern#ACKNOWLEDGED}.
091     * @param writeConcernConstantClassName The name of a class containing the aforementioned static WriteConcern
092     *                                      constant. Defaults to {@link WriteConcern}.
093     * @param databaseName The name of the MongoDB database containing the collection to which log events should be
094     *                     written. Mutually exclusive with {@code factoryClassName&factoryMethodName!=null}.
095     * @param server The host name of the MongoDB server, defaults to localhost and mutually exclusive with
096     *               {@code factoryClassName&factoryMethodName!=null}.
097     * @param port The port the MongoDB server is listening on, defaults to the default MongoDB port and mutually
098     *             exclusive with {@code factoryClassName&factoryMethodName!=null}.
099     * @param userName The username to authenticate against the MongoDB server with.
100     * @param password The password to authenticate against the MongoDB server with.
101     * @param factoryClassName A fully qualified class name containing a static factory method capable of returning a
102     *                         {@link DB} or a {@link MongoClient}.
103     * @param factoryMethodName The name of the public static factory method belonging to the aforementioned factory
104     *                          class.
105     * @return a new MongoDB provider.
106     * @deprecated in 2.8; use {@link #newBuilder()} instead.
107     */
108    @PluginFactory
109    public static MongoDbProvider createNoSqlProvider(
110            final String collectionName,
111            final String writeConcernConstant,
112            final String writeConcernConstantClassName,
113            final String databaseName,
114            final String server,
115            final String port,
116            final String userName,
117            final String password,
118            final String factoryClassName,
119                        final String factoryMethodName) {
120        LOGGER.info("createNoSqlProvider");
121                return newBuilder().setCollectionName(collectionName).setWriteConcernConstant(writeConcernConstantClassName)
122                                .setWriteConcernConstant(writeConcernConstant).setDatabaseName(databaseName).setServer(server)
123                                .setPort(port).setUserName(userName).setPassword(password).setFactoryClassName(factoryClassName)
124                                .setFactoryMethodName(factoryMethodName).build();
125        }
126
127        @PluginBuilderFactory
128        public static <B extends Builder<B>> B newBuilder() {
129                return new Builder<B>().asBuilder();
130        }
131
132        public static class Builder<B extends Builder<B>> extends AbstractFilterable.Builder<B>
133                        implements org.apache.logging.log4j.core.util.Builder<MongoDbProvider> {
134
135                @PluginBuilderAttribute
136                @ValidHost
137                private String server = "localhost";
138
139                @PluginBuilderAttribute
140                @ValidPort
141                private String port = "" + DEFAULT_PORT;
142
143                @PluginBuilderAttribute
144                @Required(message = "No database name provided")
145                private String databaseName;
146
147                @PluginBuilderAttribute
148                @Required(message = "No collection name provided")
149                private String collectionName;
150
151                @PluginBuilderAttribute
152                private String userName;
153
154                @PluginBuilderAttribute(sensitive = true)
155                private String password;
156
157                @PluginBuilderAttribute("capped")
158                private boolean isCapped = false;
159
160                @PluginBuilderAttribute
161                private int collectionSize = DEFAULT_COLLECTION_SIZE;
162
163                @PluginBuilderAttribute
164                private String factoryClassName;
165
166                @PluginBuilderAttribute
167                private String factoryMethodName;
168
169                @PluginBuilderAttribute
170                private String writeConcernConstantClassName;
171
172                @PluginBuilderAttribute
173                private String writeConcernConstant;
174
175                public B setServer(final String server) {
176                        this.server = server;
177                        return asBuilder();
178                }
179
180                public B setPort(final String port) {
181                        this.port = port;
182                        return asBuilder();
183                }
184
185                public B setDatabaseName(final String databaseName) {
186                        this.databaseName = databaseName;
187                        return asBuilder();
188                }
189
190                public B setCollectionName(final String collectionName) {
191                        this.collectionName = collectionName;
192                        return asBuilder();
193                }
194
195                public B setUserName(final String userName) {
196                        this.userName = userName;
197                        return asBuilder();
198                }
199
200                public B setPassword(final String password) {
201                        this.password = password;
202                        return asBuilder();
203                }
204
205                public B setCapped(final boolean isCapped) {
206                        this.isCapped = isCapped;
207                        return asBuilder();
208                }
209
210                public B setCollectionSize(final int collectionSize) {
211                        this.collectionSize = collectionSize;
212                        return asBuilder();
213                }
214
215                public B setFactoryClassName(final String factoryClassName) {
216                        this.factoryClassName = factoryClassName;
217                        return asBuilder();
218                }
219
220                public B setFactoryMethodName(final String factoryMethodName) {
221                        this.factoryMethodName = factoryMethodName;
222                        return asBuilder();
223                }
224
225                public B setWriteConcernConstantClassName(final String writeConcernConstantClassName) {
226                        this.writeConcernConstantClassName = writeConcernConstantClassName;
227                        return asBuilder();
228                }
229
230                public B setWriteConcernConstant(final String writeConcernConstant) {
231                        this.writeConcernConstant = writeConcernConstant;
232                        return asBuilder();
233                }
234        
235                @Override
236                public MongoDbProvider build() {
237                DB database;
238                String description;
239                if (Strings.isNotEmpty(factoryClassName) && Strings.isNotEmpty(factoryMethodName)) {
240                    try {
241                        final Class<?> factoryClass = LoaderUtil.loadClass(factoryClassName);
242                        final Method method = factoryClass.getMethod(factoryMethodName);
243                        final Object object = method.invoke(null);
244
245                        if (object instanceof DB) {
246                            database = (DB) object;
247                        } else if (object instanceof MongoClient) {
248                            if (Strings.isNotEmpty(databaseName)) {
249                                database = ((MongoClient) object).getDB(databaseName);
250                            } else {
251                                LOGGER.error("The factory method [{}.{}()] returned a MongoClient so the database name is "
252                                        + "required.", factoryClassName, factoryMethodName);
253                                return null;
254                            }
255                        } else if (object == null) {
256                            LOGGER.error("The factory method [{}.{}()] returned null.", factoryClassName, factoryMethodName);
257                            return null;
258                        } else {
259                            LOGGER.error("The factory method [{}.{}()] returned an unsupported type [{}].", factoryClassName,
260                                    factoryMethodName, object.getClass().getName());
261                            return null;
262                        }
263
264                        description = "database=" + database.getName();
265                        final List<ServerAddress> addresses = database.getMongo().getAllAddress();
266                        if (addresses.size() == 1) {
267                            description += ", server=" + addresses.get(0).getHost() + ", port=" + addresses.get(0).getPort();
268                        } else {
269                            description += ", servers=[";
270                            for (final ServerAddress address : addresses) {
271                                description += " { " + address.getHost() + ", " + address.getPort() + " } ";
272                            }
273                            description += "]";
274                        }
275                    } catch (final ClassNotFoundException e) {
276                        LOGGER.error("The factory class [{}] could not be loaded.", factoryClassName, e);
277                        return null;
278                    } catch (final NoSuchMethodException e) {
279                        LOGGER.error("The factory class [{}] does not have a no-arg method named [{}].", factoryClassName,
280                                factoryMethodName, e);
281                        return null;
282                    } catch (final Exception e) {
283                        LOGGER.error("The factory method [{}.{}()] could not be invoked.", factoryClassName, factoryMethodName,
284                                e);
285                        return null;
286                    }
287                } else if (Strings.isNotEmpty(databaseName)) {
288                    final List<MongoCredential> credentials = new ArrayList<>();
289                    description = "database=" + databaseName;
290                    if (Strings.isNotEmpty(userName) && Strings.isNotEmpty(password)) {
291                        description += ", username=" + userName + ", passwordHash="
292                                + NameUtil.md5(password + MongoDbProvider.class.getName());
293                        credentials.add(MongoCredential.createCredential(userName, databaseName, password.toCharArray()));
294                    }
295                    try {
296                        final int portInt = TypeConverters.convert(port, int.class, DEFAULT_PORT);
297                        description += ", server=" + server + ", port=" + portInt;
298                        database = new MongoClient(new ServerAddress(server, portInt), credentials).getDB(databaseName);
299                    } catch (final Exception e) {
300                        LOGGER.error(
301                                "Failed to obtain a database instance from the MongoClient at server [{}] and " + "port [{}].",
302                                server, port);
303                        return null;
304                    }
305                } else {
306                    LOGGER.error("No factory method was provided so the database name is required.");
307                    return null;
308                }
309
310                try {
311                    database.getCollectionNames(); // Check if the database actually requires authentication
312                } catch (final Exception e) {
313                    LOGGER.error(
314                            "The database is not up, or you are not authenticated, try supplying a username and password to the MongoDB provider.",
315                            e);
316                    return null;
317                }
318
319                final WriteConcern writeConcern = toWriteConcern(writeConcernConstant, writeConcernConstantClassName);
320
321                return new MongoDbProvider(database, writeConcern, collectionName, isCapped, collectionSize, description);
322                }
323
324            private static WriteConcern toWriteConcern(final String writeConcernConstant,
325                    final String writeConcernConstantClassName) {
326                WriteConcern writeConcern;
327                if (Strings.isNotEmpty(writeConcernConstant)) {
328                    if (Strings.isNotEmpty(writeConcernConstantClassName)) {
329                        try {
330                            final Class<?> writeConcernConstantClass = LoaderUtil.loadClass(writeConcernConstantClassName);
331                            final Field field = writeConcernConstantClass.getField(writeConcernConstant);
332                            writeConcern = (WriteConcern) field.get(null);
333                        } catch (final Exception e) {
334                            LOGGER.error("Write concern constant [{}.{}] not found, using default.",
335                                    writeConcernConstantClassName, writeConcernConstant);
336                            writeConcern = DEFAULT_WRITE_CONCERN;
337                        }
338                    } else {
339                        writeConcern = WriteConcern.valueOf(writeConcernConstant);
340                        if (writeConcern == null) {
341                            LOGGER.warn("Write concern constant [{}] not found, using default.", writeConcernConstant);
342                            writeConcern = DEFAULT_WRITE_CONCERN;
343                        }
344                    }
345                } else {
346                    writeConcern = DEFAULT_WRITE_CONCERN;
347                }
348                return writeConcern;
349            }
350    }
351}