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