001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Portions Copyright 2013-2015 ForgeRock AS
025 */
026package org.opends.server.loggers;
027
028import static org.opends.messages.ConfigMessages.*;
029import static org.opends.server.util.StaticUtils.*;
030
031import java.io.File;
032import java.io.IOException;
033import java.text.SimpleDateFormat;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042
043import org.forgerock.i18n.LocalizableMessage;
044import org.forgerock.opendj.config.server.ConfigChangeResult;
045import org.forgerock.opendj.config.server.ConfigException;
046import org.forgerock.util.Utils;
047import org.opends.server.admin.server.ConfigurationChangeListener;
048import org.opends.server.admin.std.server.FileBasedHTTPAccessLogPublisherCfg;
049import org.opends.server.core.DirectoryServer;
050import org.opends.server.core.ServerContext;
051import org.opends.server.types.DN;
052import org.opends.server.types.DirectoryException;
053import org.opends.server.types.FilePermission;
054import org.opends.server.types.InitializationException;
055import org.opends.server.util.TimeThread;
056
057/**
058 * This class provides the implementation of the HTTP access logger used by the
059 * directory server.
060 */
061public final class TextHTTPAccessLogPublisher extends
062    HTTPAccessLogPublisher<FileBasedHTTPAccessLogPublisherCfg>
063    implements ConfigurationChangeListener<FileBasedHTTPAccessLogPublisherCfg>
064{
065
066  // Extended log format standard fields
067  private static final String ELF_C_IP = "c-ip";
068  private static final String ELF_C_PORT = "c-port";
069  private static final String ELF_CS_HOST = "cs-host";
070  private static final String ELF_CS_METHOD = "cs-method";
071  private static final String ELF_CS_URI_QUERY = "cs-uri-query";
072  private static final String ELF_CS_USER_AGENT = "cs(User-Agent)";
073  private static final String ELF_CS_USERNAME = "cs-username";
074  private static final String ELF_CS_VERSION = "cs-version";
075  private static final String ELF_S_COMPUTERNAME = "s-computername";
076  private static final String ELF_S_IP = "s-ip";
077  private static final String ELF_S_PORT = "s-port";
078  private static final String ELF_SC_STATUS = "sc-status";
079  // Application specific fields (eXtensions)
080  private static final String X_CONNECTION_ID = "x-connection-id";
081  private static final String X_DATETIME = "x-datetime";
082  private static final String X_ETIME = "x-etime";
083  private static final String X_TRANSACTION_ID = "x-transaction-id";
084
085  private static final Set<String> ALL_SUPPORTED_FIELDS = new HashSet<>(
086      Arrays.asList(ELF_C_IP, ELF_C_PORT, ELF_CS_HOST, ELF_CS_METHOD,
087          ELF_CS_URI_QUERY, ELF_CS_USER_AGENT, ELF_CS_USERNAME, ELF_CS_VERSION,
088          ELF_S_COMPUTERNAME, ELF_S_IP, ELF_S_PORT, ELF_SC_STATUS,
089          X_CONNECTION_ID, X_DATETIME, X_ETIME, X_TRANSACTION_ID));
090
091  /**
092   * Returns an instance of the text HTTP access log publisher that will print
093   * all messages to the provided writer. This is used to print the messages to
094   * the console when the server starts up.
095   *
096   * @param writer
097   *          The text writer where the message will be written to.
098   * @return The instance of the text error log publisher that will print all
099   *         messages to standard out.
100   */
101  public static TextHTTPAccessLogPublisher getStartupTextHTTPAccessPublisher(
102      final TextWriter writer)
103  {
104    final TextHTTPAccessLogPublisher startupPublisher = new TextHTTPAccessLogPublisher();
105    startupPublisher.writer = writer;
106    return startupPublisher;
107  }
108
109  private TextWriter writer;
110  private FileBasedHTTPAccessLogPublisherCfg cfg;
111  private List<String> logFormatFields;
112  private String timeStampFormat = "dd/MMM/yyyy:HH:mm:ss Z";
113
114  @Override
115  public ConfigChangeResult applyConfigurationChange(final FileBasedHTTPAccessLogPublisherCfg config)
116  {
117    final ConfigChangeResult ccr = new ConfigChangeResult();
118
119    try
120    {
121      // Determine the writer we are using. If we were writing asynchronously,
122      // we need to modify the underlying writer.
123      TextWriter currentWriter;
124      if (writer instanceof AsynchronousTextWriter)
125      {
126        currentWriter = ((AsynchronousTextWriter) writer).getWrappedWriter();
127      }
128      else
129      {
130        currentWriter = writer;
131      }
132
133      if (currentWriter instanceof MultifileTextWriter)
134      {
135        final MultifileTextWriter mfWriter = (MultifileTextWriter) currentWriter;
136        configure(mfWriter, config);
137
138        if (config.isAsynchronous())
139        {
140          if (writer instanceof AsynchronousTextWriter)
141          {
142            if (hasAsyncConfigChanged(config))
143            {
144              // reinstantiate
145              final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
146              writer = newAsyncWriter(mfWriter, config);
147              previousWriter.shutdown(false);
148            }
149          }
150          else
151          {
152            // turn async text writer on
153            writer = newAsyncWriter(mfWriter, config);
154          }
155        }
156        else
157        {
158          if (writer instanceof AsynchronousTextWriter)
159          {
160            // asynchronous is being turned off, remove async text writers.
161            final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
162            writer = mfWriter;
163            previousWriter.shutdown(false);
164          }
165        }
166
167        if (cfg.isAsynchronous() && config.isAsynchronous()
168            && cfg.getQueueSize() != config.getQueueSize())
169        {
170          ccr.setAdminActionRequired(true);
171        }
172
173        if (!config.getLogRecordTimeFormat().equals(timeStampFormat))
174        {
175          TimeThread.removeUserDefinedFormatter(timeStampFormat);
176          timeStampFormat = config.getLogRecordTimeFormat();
177        }
178
179        cfg = config;
180        logFormatFields = extractFieldsOrder(cfg.getLogFormat());
181        LocalizableMessage errorMessage = validateLogFormat(logFormatFields);
182        if (errorMessage != null)
183        {
184          ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
185          ccr.setAdminActionRequired(true);
186          ccr.addMessage(errorMessage);
187        }
188      }
189    }
190    catch (final Exception e)
191    {
192      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
193      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
194          config.dn(), stackTraceToSingleLineString(e)));
195    }
196
197    return ccr;
198  }
199
200  private void configure(MultifileTextWriter mfWriter, FileBasedHTTPAccessLogPublisherCfg config)
201      throws DirectoryException
202  {
203    final FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
204    final boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
205
206    final File logFile = getLogFile(config);
207    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
208
209    mfWriter.setNamingPolicy(fnPolicy);
210    mfWriter.setFilePermissions(perm);
211    mfWriter.setAppend(config.isAppend());
212    mfWriter.setAutoFlush(writerAutoFlush);
213    mfWriter.setBufferSize((int) config.getBufferSize());
214    mfWriter.setInterval(config.getTimeInterval());
215
216    mfWriter.removeAllRetentionPolicies();
217    mfWriter.removeAllRotationPolicies();
218    for (final DN dn : config.getRotationPolicyDNs())
219    {
220      mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
221    }
222    for (final DN dn : config.getRetentionPolicyDNs())
223    {
224      mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
225    }
226  }
227
228  private File getLogFile(final FileBasedHTTPAccessLogPublisherCfg config)
229  {
230    return getFileForPath(config.getLogFile());
231  }
232
233  private boolean hasAsyncConfigChanged(FileBasedHTTPAccessLogPublisherCfg newConfig)
234  {
235    return hasParallelConfigChanged(newConfig) && cfg.getQueueSize() != newConfig.getQueueSize();
236  }
237
238  private boolean hasParallelConfigChanged(FileBasedHTTPAccessLogPublisherCfg newConfig)
239  {
240    return !cfg.dn().equals(newConfig.dn()) && cfg.isAutoFlush() != newConfig.isAutoFlush();
241  }
242
243  private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter mfWriter, FileBasedHTTPAccessLogPublisherCfg config)
244  {
245    String name = "Asynchronous Text Writer for " + config.dn();
246    return new AsynchronousTextWriter(name, config.getQueueSize(), config.isAutoFlush(), mfWriter);
247  }
248
249  private List<String> extractFieldsOrder(String logFormat)
250  {
251    // there will always be at least one field value due to the regexp
252    // validating the log format
253    return Arrays.asList(logFormat.split(" "));
254  }
255
256  /**
257   * Validates the provided fields for the log format.
258   *
259   * @param fields
260   *          the fields comprising the log format.
261   * @return an error message when validation fails, null otherwise
262   */
263  private LocalizableMessage validateLogFormat(List<String> fields)
264  {
265    final Collection<String> unsupportedFields =
266        subtract(fields, ALL_SUPPORTED_FIELDS);
267    if (!unsupportedFields.isEmpty())
268    { // there are some unsupported fields. List them.
269      return WARN_CONFIG_LOGGING_UNSUPPORTED_FIELDS_IN_LOG_FORMAT.get(
270          cfg.dn(), Utils.joinAsString(", ", unsupportedFields));
271    }
272    if (fields.size() == unsupportedFields.size())
273    { // all fields are unsupported
274      return ERR_CONFIG_LOGGING_EMPTY_LOG_FORMAT.get(cfg.dn());
275    }
276    return null;
277  }
278
279  /**
280   * Returns a new Collection containing a - b.
281   *
282   * @param <T>
283   * @param a
284   *          the collection to subtract from, must not be null
285   * @param b
286   *          the collection to subtract, must not be null
287   * @return a new collection with the results
288   */
289  private <T> Collection<T> subtract(Collection<T> a, Collection<T> b)
290  {
291    final Collection<T> result = new ArrayList<>();
292    for (T elem : a)
293    {
294      if (!b.contains(elem))
295      {
296        result.add(elem);
297      }
298    }
299    return result;
300  }
301
302  @Override
303  public void initializeLogPublisher(
304      final FileBasedHTTPAccessLogPublisherCfg cfg, ServerContext serverContext)
305      throws ConfigException, InitializationException
306  {
307    final File logFile = getLogFile(cfg);
308    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
309
310    try
311    {
312      final FilePermission perm = FilePermission.decodeUNIXMode(cfg.getLogFilePermissions());
313      final LogPublisherErrorHandler errorHandler = new LogPublisherErrorHandler(cfg.dn());
314      final boolean writerAutoFlush = cfg.isAutoFlush() && !cfg.isAsynchronous();
315
316      final MultifileTextWriter theWriter = new MultifileTextWriter(
317          "Multifile Text Writer for " + cfg.dn(),
318          cfg.getTimeInterval(), fnPolicy, perm, errorHandler, "UTF-8",
319          writerAutoFlush, cfg.isAppend(), (int) cfg.getBufferSize());
320
321      // Validate retention and rotation policies.
322      for (final DN dn : cfg.getRotationPolicyDNs())
323      {
324        theWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
325      }
326      for (final DN dn : cfg.getRetentionPolicyDNs())
327      {
328        theWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
329      }
330
331      if (cfg.isAsynchronous())
332      {
333        this.writer = newAsyncWriter(theWriter, cfg);
334      }
335      else
336      {
337        this.writer = theWriter;
338      }
339    }
340    catch (final DirectoryException e)
341    {
342      throw new InitializationException(
343          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg.dn(), e), e);
344    }
345    catch (final IOException e)
346    {
347      throw new InitializationException(
348          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, cfg.dn(), e), e);
349    }
350
351    this.cfg = cfg;
352    logFormatFields = extractFieldsOrder(cfg.getLogFormat());
353    LocalizableMessage error = validateLogFormat(logFormatFields);
354    if (error != null)
355    {
356      throw new InitializationException(error);
357    }
358    timeStampFormat = cfg.getLogRecordTimeFormat();
359
360    cfg.addFileBasedHTTPAccessChangeListener(this);
361  }
362
363  @Override
364  public boolean isConfigurationAcceptable(
365      final FileBasedHTTPAccessLogPublisherCfg configuration,
366      final List<LocalizableMessage> unacceptableReasons)
367  {
368    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
369  }
370
371  @Override
372  public boolean isConfigurationChangeAcceptable(
373      final FileBasedHTTPAccessLogPublisherCfg config,
374      final List<LocalizableMessage> unacceptableReasons)
375  {
376    // Validate the time-stamp formatter.
377    final String formatString = config.getLogRecordTimeFormat();
378    try
379    {
380       new SimpleDateFormat(formatString);
381    }
382    catch (final Exception e)
383    {
384      unacceptableReasons.add(ERR_CONFIG_LOGGING_INVALID_TIME_FORMAT.get(formatString));
385      return false;
386    }
387
388    // Make sure the permission is valid.
389    try
390    {
391      final FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
392      if (!filePerm.isOwnerWritable())
393      {
394        final LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions());
395        unacceptableReasons.add(message);
396        return false;
397      }
398    }
399    catch (final DirectoryException e)
400    {
401      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
402      return false;
403    }
404
405    return true;
406  }
407
408  @Override
409  public final void close()
410  {
411    writer.shutdown();
412    TimeThread.removeUserDefinedFormatter(timeStampFormat);
413    if (cfg != null)
414    {
415      cfg.removeFileBasedHTTPAccessChangeListener(this);
416    }
417  }
418
419  @Override
420  public final DN getDN()
421  {
422    return cfg != null ? cfg.dn() : null;
423  }
424
425  @Override
426  public void logRequestInfo(HTTPRequestInfo ri)
427  {
428    final Map<String, Object> fields = new HashMap<>();
429    fields.put(ELF_C_IP, ri.getClientAddress());
430    fields.put(ELF_C_PORT, ri.getClientPort());
431    fields.put(ELF_CS_HOST, ri.getClientHost());
432    fields.put(ELF_CS_METHOD, ri.getMethod());
433    fields.put(ELF_CS_URI_QUERY, ri.getQuery());
434    fields.put(ELF_CS_USER_AGENT, ri.getUserAgent());
435    fields.put(ELF_CS_USERNAME, ri.getAuthUser());
436    fields.put(ELF_CS_VERSION, ri.getProtocol());
437    fields.put(ELF_S_IP, ri.getServerAddress());
438    fields.put(ELF_S_COMPUTERNAME, ri.getServerHost());
439    fields.put(ELF_S_PORT, ri.getServerPort());
440    fields.put(ELF_SC_STATUS, ri.getStatusCode());
441    fields.put(X_CONNECTION_ID, ri.getConnectionID());
442    fields.put(X_DATETIME, TimeThread.getUserDefinedTime(timeStampFormat));
443    fields.put(X_ETIME, ri.getTotalProcessingTime());
444    fields.put(X_TRANSACTION_ID, ri.getTransactionId());
445
446    writeLogRecord(fields, logFormatFields);
447  }
448
449  private void writeLogRecord(Map<String, Object> fields,
450      List<String> fieldnames)
451  {
452    if (fieldnames == null)
453    {
454      return;
455    }
456    final StringBuilder sb = new StringBuilder(100);
457    for (String fieldname : fieldnames)
458    {
459      append(sb, fields.get(fieldname));
460    }
461    writer.writeRecord(sb.toString());
462  }
463
464  /**
465   * Appends the value to the string builder using the default separator if needed.
466   *
467   * @param sb
468   *          the StringBuilder where to append.
469   * @param value
470   *          the value to append.
471   */
472  private void append(final StringBuilder sb, Object value)
473  {
474    final char separator = '\t'; // as encouraged by the W3C working draft
475    if (sb.length() > 0)
476    {
477      sb.append(separator);
478    }
479
480    if (value != null)
481    {
482      String val = String.valueOf(value);
483      boolean useQuotes = val.contains(Character.toString(separator));
484      if (useQuotes)
485      {
486        sb.append('"').append(val.replaceAll("\"", "\"\"")).append('"');
487      }
488      else
489      {
490        sb.append(val);
491      }
492    }
493    else
494    {
495      sb.append('-');
496    }
497  }
498}