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