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