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 *      Copyright 2006-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.loggers;
028
029import static org.opends.messages.LoggerMessages.ERR_COMMON_AUDIT_INVALID_TRANSACTION_ID;
030import static org.opends.messages.ConfigMessages.*;
031import static org.opends.server.util.StaticUtils.*;
032
033import java.io.File;
034import java.io.IOException;
035import java.text.SimpleDateFormat;
036import java.util.List;
037import java.util.Set;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.LocalizableMessageBuilder;
041import org.forgerock.opendj.config.server.ConfigChangeResult;
042import org.forgerock.opendj.config.server.ConfigException;
043import org.forgerock.opendj.ldap.ByteString;
044import org.forgerock.opendj.ldap.ResultCode;
045import org.forgerock.util.Utils;
046import org.opends.server.admin.server.ConfigurationChangeListener;
047import org.opends.server.admin.std.meta.FileBasedAccessLogPublisherCfgDefn.LogFormat;
048import org.opends.server.admin.std.server.FileBasedAccessLogPublisherCfg;
049import org.opends.server.api.ClientConnection;
050import org.opends.server.api.ExtendedOperationHandler;
051import org.opends.server.controls.TransactionIdControl;
052import org.opends.server.core.*;
053import org.opends.server.types.*;
054import org.opends.server.util.StaticUtils;
055import org.opends.server.util.TimeThread;
056
057/** This class provides the implementation of the access logger used by the directory server. */
058public final class TextAccessLogPublisher extends
059    AbstractTextAccessLogPublisher<FileBasedAccessLogPublisherCfg> implements
060    ConfigurationChangeListener<FileBasedAccessLogPublisherCfg>
061{
062  /** The category to use when logging responses. */
063  private static final String CATEGORY_RESPONSE = "RES";
064  /** The category to use when logging requests. */
065  private static final String CATEGORY_REQUEST = "REQ";
066
067  /**
068   * Returns an instance of the text access log publisher that will print all
069   * messages to the provided writer. This is used to print the messages to the
070   * console when the server starts up.
071   *
072   * @param writer
073   *          The text writer where the message will be written to.
074   * @param suppressInternal
075   *          Indicates whether to suppress internal operations.
076   * @return The instance of the text error log publisher that will print all
077   *         messages to standard out.
078   */
079  public static TextAccessLogPublisher getStartupTextAccessPublisher(
080      final TextWriter writer, final boolean suppressInternal)
081  {
082    final TextAccessLogPublisher startupPublisher = new TextAccessLogPublisher();
083    startupPublisher.writer = writer;
084    startupPublisher.buildFilters(suppressInternal);
085    return startupPublisher;
086  }
087
088  private TextWriter writer;
089  private FileBasedAccessLogPublisherCfg cfg;
090  private boolean isCombinedMode;
091  private boolean includeControlOIDs;
092  private String timeStampFormat = "dd/MMM/yyyy:HH:mm:ss Z";
093  private ServerContext serverContext;
094
095  @Override
096  public ConfigChangeResult applyConfigurationChange(FileBasedAccessLogPublisherCfg config)
097  {
098    final ConfigChangeResult ccr = new ConfigChangeResult();
099
100    try
101    {
102      // Determine the writer we are using. If we were writing asynchronously,
103      // we need to modify the underlying writer.
104      TextWriter currentWriter;
105      if (writer instanceof AsynchronousTextWriter)
106      {
107        currentWriter = ((AsynchronousTextWriter) writer).getWrappedWriter();
108      }
109      else
110      {
111        currentWriter = writer;
112      }
113
114      if (currentWriter instanceof MultifileTextWriter)
115      {
116        final MultifileTextWriter mfWriter = (MultifileTextWriter) currentWriter;
117        configure(mfWriter, config);
118
119        if (config.isAsynchronous())
120        {
121          if (writer instanceof AsynchronousTextWriter)
122          {
123            if (hasAsyncConfigChanged(config))
124            {
125              // reinstantiate
126              final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
127              writer = newAsyncWriter(mfWriter, config);
128              previousWriter.shutdown(false);
129            }
130          }
131          else
132          {
133            // turn async text writer on
134            writer = newAsyncWriter(mfWriter, config);
135          }
136        }
137        else
138        {
139          if (writer instanceof AsynchronousTextWriter)
140          {
141            // asynchronous is being turned off, remove async text writers.
142            final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
143            writer = mfWriter;
144            previousWriter.shutdown(false);
145          }
146        }
147
148        if (cfg.isAsynchronous() && config.isAsynchronous()
149            && cfg.getQueueSize() != config.getQueueSize())
150        {
151          ccr.setAdminActionRequired(true);
152        }
153
154        if (!config.getLogRecordTimeFormat().equals(timeStampFormat))
155        {
156          TimeThread.removeUserDefinedFormatter(timeStampFormat);
157          timeStampFormat = config.getLogRecordTimeFormat();
158        }
159
160        cfg = config;
161        isCombinedMode = cfg.getLogFormat() == LogFormat.COMBINED;
162        includeControlOIDs = cfg.isLogControlOids();
163      }
164    }
165    catch (final Exception e)
166    {
167      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
168      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
169          config.dn(), stackTraceToSingleLineString(e)));
170    }
171
172    return ccr;
173  }
174
175  private void configure(MultifileTextWriter mfWriter, FileBasedAccessLogPublisherCfg config) throws DirectoryException
176  {
177    final FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
178    final boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
179
180    final File logFile = getLogFile(config);
181    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
182
183    mfWriter.setNamingPolicy(fnPolicy);
184    mfWriter.setFilePermissions(perm);
185    mfWriter.setAppend(config.isAppend());
186    mfWriter.setAutoFlush(writerAutoFlush);
187    mfWriter.setBufferSize((int) config.getBufferSize());
188    mfWriter.setInterval(config.getTimeInterval());
189
190    mfWriter.removeAllRetentionPolicies();
191    mfWriter.removeAllRotationPolicies();
192    for (final DN dn : config.getRotationPolicyDNs())
193    {
194      mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
195    }
196    for (final DN dn : config.getRetentionPolicyDNs())
197    {
198      mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
199    }
200  }
201
202  private File getLogFile(final FileBasedAccessLogPublisherCfg config)
203  {
204    return getFileForPath(config.getLogFile());
205  }
206
207  @Override
208  public void initializeLogPublisher(final FileBasedAccessLogPublisherCfg cfg, ServerContext serverContext)
209      throws ConfigException, InitializationException
210  {
211    this.serverContext = serverContext;
212    final File logFile = getLogFile(cfg);
213    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
214
215    try
216    {
217      final FilePermission perm = FilePermission.decodeUNIXMode(cfg.getLogFilePermissions());
218      final LogPublisherErrorHandler errorHandler = new LogPublisherErrorHandler(cfg.dn());
219      final boolean writerAutoFlush = cfg.isAutoFlush() && !cfg.isAsynchronous();
220
221      final MultifileTextWriter theWriter = new MultifileTextWriter(
222          "Multifile Text Writer for " + cfg.dn(),
223          cfg.getTimeInterval(), fnPolicy, perm, errorHandler, "UTF-8",
224          writerAutoFlush, cfg.isAppend(), (int) cfg.getBufferSize());
225
226      // Validate retention and rotation policies.
227      for (final DN dn : cfg.getRotationPolicyDNs())
228      {
229        theWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
230      }
231      for (final DN dn : cfg.getRetentionPolicyDNs())
232      {
233        theWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
234      }
235
236      if (cfg.isAsynchronous())
237      {
238        this.writer = newAsyncWriter(theWriter, cfg);
239      }
240      else
241      {
242        this.writer = theWriter;
243      }
244    }
245    catch (final DirectoryException e)
246    {
247      throw new InitializationException(
248          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg.dn(), e), e);
249    }
250    catch (final IOException e)
251    {
252      throw new InitializationException(
253          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, cfg.dn(), e), e);
254    }
255
256    initializeFilters(cfg);
257
258    this.cfg = cfg;
259    isCombinedMode = cfg.getLogFormat() == LogFormat.COMBINED;
260    includeControlOIDs = cfg.isLogControlOids();
261    timeStampFormat = cfg.getLogRecordTimeFormat();
262
263    cfg.addFileBasedAccessChangeListener(this);
264  }
265
266  private boolean hasAsyncConfigChanged(FileBasedAccessLogPublisherCfg newConfig)
267  {
268    return hasParallelConfigChanged(newConfig) && cfg.getQueueSize() != newConfig.getQueueSize();
269  }
270
271  private boolean hasParallelConfigChanged(FileBasedAccessLogPublisherCfg newConfig)
272  {
273    return !cfg.dn().equals(newConfig.dn()) && cfg.isAutoFlush() != newConfig.isAutoFlush();
274  }
275
276  private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter mfWriter, FileBasedAccessLogPublisherCfg config)
277  {
278    String name = "Asynchronous Text Writer for " + config.dn();
279    return new AsynchronousTextWriter(name, config.getQueueSize(), config.isAutoFlush(), mfWriter);
280  }
281
282  @Override
283  public boolean isConfigurationAcceptable(
284      final FileBasedAccessLogPublisherCfg configuration,
285      final List<LocalizableMessage> unacceptableReasons)
286  {
287    return isFilterConfigurationAcceptable(configuration, unacceptableReasons)
288        && isConfigurationChangeAcceptable(configuration, unacceptableReasons);
289  }
290
291  @Override
292  public boolean isConfigurationChangeAcceptable(
293      final FileBasedAccessLogPublisherCfg config,
294      final List<LocalizableMessage> unacceptableReasons)
295  {
296    // Validate the time-stamp formatter.
297    final String formatString = config.getLogRecordTimeFormat();
298    try
299    {
300       new SimpleDateFormat(formatString);
301    }
302    catch (final Exception e)
303    {
304      final LocalizableMessage message = ERR_CONFIG_LOGGING_INVALID_TIME_FORMAT.get(formatString);
305      unacceptableReasons.add(message);
306      return false;
307    }
308
309    // Make sure the permission is valid.
310    try
311    {
312      final FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
313      if (!filePerm.isOwnerWritable())
314      {
315        final LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions());
316        unacceptableReasons.add(message);
317        return false;
318      }
319    }
320    catch (final DirectoryException e)
321    {
322      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
323      return false;
324    }
325
326    return true;
327  }
328
329  @Override
330  public void logAbandonRequest(final AbandonOperation abandonOperation)
331  {
332    if (isCombinedMode || !isRequestLoggable(abandonOperation))
333    {
334      return;
335    }
336
337    final StringBuilder buffer = new StringBuilder(100);
338    appendHeader(abandonOperation, "ABANDON", CATEGORY_REQUEST, buffer);
339    appendAbandonRequest(abandonOperation, buffer);
340    writer.writeRecord(buffer.toString());
341  }
342
343  @Override
344  public void logAbandonResult(final AbandonOperation abandonOperation)
345  {
346    if (!isResponseLoggable(abandonOperation))
347    {
348      return;
349    }
350
351    final StringBuilder buffer = new StringBuilder(100);
352    appendHeader(abandonOperation, "ABANDON", CATEGORY_RESPONSE, buffer);
353    if (isCombinedMode)
354    {
355      appendAbandonRequest(abandonOperation, buffer);
356    }
357    appendResultCodeAndMessage(buffer, abandonOperation);
358    logAdditionalLogItems(abandonOperation, buffer);
359    appendEtime(buffer, abandonOperation);
360    writer.writeRecord(buffer.toString());
361  }
362
363  @Override
364  public void logAddRequest(final AddOperation addOperation)
365  {
366    if (isCombinedMode || !isRequestLoggable(addOperation))
367    {
368      return;
369    }
370
371    final StringBuilder buffer = new StringBuilder(100);
372    appendHeader(addOperation, "ADD", CATEGORY_REQUEST, buffer);
373    appendAddRequest(addOperation, buffer);
374    writer.writeRecord(buffer.toString());
375  }
376
377  @Override
378  public void logAddResponse(final AddOperation addOperation)
379  {
380    if (!isResponseLoggable(addOperation))
381    {
382      return;
383    }
384
385    final StringBuilder buffer = new StringBuilder(100);
386    appendHeader(addOperation, "ADD", CATEGORY_RESPONSE, buffer);
387    if (isCombinedMode)
388    {
389      appendAddRequest(addOperation, buffer);
390    }
391    appendResultCodeAndMessage(buffer, addOperation);
392    logAdditionalLogItems(addOperation, buffer);
393    appendLabelIfNotNull(buffer, "authzDN", addOperation.getProxiedAuthorizationDN());
394    appendEtime(buffer, addOperation);
395    writer.writeRecord(buffer.toString());
396  }
397
398  @Override
399  public void logBindRequest(final BindOperation bindOperation)
400  {
401    if (isCombinedMode || !isRequestLoggable(bindOperation))
402    {
403      return;
404    }
405
406    final StringBuilder buffer = new StringBuilder(100);
407    appendHeader(bindOperation, "BIND", CATEGORY_REQUEST, buffer);
408    appendBindRequest(bindOperation, buffer);
409    writer.writeRecord(buffer.toString());
410  }
411
412  @Override
413  public void logBindResponse(final BindOperation bindOperation)
414  {
415    if (!isResponseLoggable(bindOperation))
416    {
417      return;
418    }
419
420    final StringBuilder buffer = new StringBuilder(100);
421    appendHeader(bindOperation, "BIND", CATEGORY_RESPONSE, buffer);
422    if (isCombinedMode)
423    {
424      appendBindRequest(bindOperation, buffer);
425    }
426    appendResultCodeAndMessage(buffer, bindOperation);
427
428    final LocalizableMessage failureMessage = bindOperation.getAuthFailureReason();
429    if (failureMessage != null)
430    {
431      // this code path is mutually exclusive with the if result code is success down below
432      appendLabel(buffer, "authFailureReason", failureMessage);
433      if (bindOperation.getSASLMechanism() != null
434          && bindOperation.getSASLAuthUserEntry() != null)
435      { // SASL bind and we have successfully found a user entry for auth
436        appendLabel(buffer, "authDN", bindOperation.getSASLAuthUserEntry().getName());
437      }
438      else
439      { // SASL bind failed to find user entry for auth or simple bind
440        appendLabel(buffer, "authDN", bindOperation.getRawBindDN());
441      }
442    }
443
444    logAdditionalLogItems(bindOperation, buffer);
445
446    if (bindOperation.getResultCode() == ResultCode.SUCCESS)
447    {
448      // this code path is mutually exclusive with the if failure message that exists just above
449      final AuthenticationInfo authInfo = bindOperation.getAuthenticationInfo();
450      if (authInfo != null)
451      {
452        final DN authDN = authInfo.getAuthenticationDN();
453        if (authDN != null)
454        {
455          appendLabel(buffer, "authDN", authDN);
456
457          final DN authzDN = authInfo.getAuthorizationDN();
458          if (!authDN.equals(authzDN))
459          {
460            appendLabel(buffer, "authzDN", authzDN);
461          }
462        }
463        else
464        {
465          buffer.append(" authDN=\"\"");
466        }
467      }
468    }
469
470    appendEtime(buffer, bindOperation);
471
472    writer.writeRecord(buffer.toString());
473  }
474
475  @Override
476  public void logCompareRequest(final CompareOperation compareOperation)
477  {
478    if (isCombinedMode || !isRequestLoggable(compareOperation))
479    {
480      return;
481    }
482
483    final StringBuilder buffer = new StringBuilder(100);
484    appendHeader(compareOperation, "COMPARE", CATEGORY_REQUEST, buffer);
485    appendCompareRequest(compareOperation, buffer);
486    writer.writeRecord(buffer.toString());
487  }
488
489  @Override
490  public void logCompareResponse(final CompareOperation compareOperation)
491  {
492    if (!isResponseLoggable(compareOperation))
493    {
494      return;
495    }
496
497    final StringBuilder buffer = new StringBuilder(100);
498    appendHeader(compareOperation, "COMPARE", CATEGORY_RESPONSE, buffer);
499    if (isCombinedMode)
500    {
501      appendCompareRequest(compareOperation, buffer);
502    }
503    appendResultCodeAndMessage(buffer, compareOperation);
504    logAdditionalLogItems(compareOperation, buffer);
505    appendLabelIfNotNull(buffer, "authzDN", compareOperation.getProxiedAuthorizationDN());
506    appendEtime(buffer, compareOperation);
507    writer.writeRecord(buffer.toString());
508  }
509
510  @Override
511  public void logConnect(final ClientConnection clientConnection)
512  {
513    if (!isConnectLoggable(clientConnection))
514    {
515      return;
516    }
517
518    final long connectionID = clientConnection.getConnectionID();
519    final StringBuilder buffer = new StringBuilder(100);
520    buffer.append('[').append(TimeThread.getUserDefinedTime(timeStampFormat)).append(']');
521    buffer.append(" CONNECT conn=").append(connectionID);
522    buffer.append(" from=").append(clientConnection.getClientHostPort());
523    buffer.append(" to=").append(clientConnection.getServerHostPort());
524    buffer.append(" protocol=").append(clientConnection.getProtocol());
525
526    writer.writeRecord(buffer.toString());
527  }
528
529  @Override
530  public void logDeleteRequest(final DeleteOperation deleteOperation)
531  {
532    if (isCombinedMode || !isRequestLoggable(deleteOperation))
533    {
534      return;
535    }
536
537    final StringBuilder buffer = new StringBuilder(100);
538    appendHeader(deleteOperation, "DELETE", CATEGORY_REQUEST, buffer);
539    appendDeleteRequest(deleteOperation, buffer);
540    writer.writeRecord(buffer.toString());
541  }
542
543  @Override
544  public void logDeleteResponse(final DeleteOperation deleteOperation)
545  {
546    if (!isResponseLoggable(deleteOperation))
547    {
548      return;
549    }
550
551    final StringBuilder buffer = new StringBuilder(100);
552    appendHeader(deleteOperation, "DELETE", CATEGORY_RESPONSE, buffer);
553    if (isCombinedMode)
554    {
555      appendDeleteRequest(deleteOperation, buffer);
556    }
557    appendResultCodeAndMessage(buffer, deleteOperation);
558    logAdditionalLogItems(deleteOperation, buffer);
559    appendLabelIfNotNull(buffer, "authzDN", deleteOperation.getProxiedAuthorizationDN());
560    appendEtime(buffer, deleteOperation);
561    writer.writeRecord(buffer.toString());
562  }
563
564  @Override
565  public void logDisconnect(final ClientConnection clientConnection,
566      final DisconnectReason disconnectReason, final LocalizableMessage message)
567  {
568    if (!isDisconnectLoggable(clientConnection))
569    {
570      return;
571    }
572
573    final long connectionID = clientConnection.getConnectionID();
574    final StringBuilder buffer = new StringBuilder(100);
575    buffer.append('[').append(TimeThread.getUserDefinedTime(timeStampFormat)).append(']');
576    buffer.append(" DISCONNECT conn=").append(connectionID);
577    appendLabel(buffer, "reason", disconnectReason);
578    appendLabelIfNotNull(buffer, "msg", message);
579
580    writer.writeRecord(buffer.toString());
581  }
582
583  @Override
584  public void logExtendedRequest(final ExtendedOperation extendedOperation)
585  {
586    if (isCombinedMode || !isRequestLoggable(extendedOperation))
587    {
588      return;
589    }
590
591    final StringBuilder buffer = new StringBuilder(100);
592    appendHeader(extendedOperation, "EXTENDED", CATEGORY_REQUEST, buffer);
593    appendExtendedRequest(extendedOperation, buffer);
594    writer.writeRecord(buffer.toString());
595  }
596
597  @Override
598  public void logExtendedResponse(final ExtendedOperation extendedOperation)
599  {
600    if (!isResponseLoggable(extendedOperation))
601    {
602      return;
603    }
604
605    final StringBuilder buffer = new StringBuilder(100);
606    appendHeader(extendedOperation, "EXTENDED", CATEGORY_RESPONSE, buffer);
607    if (isCombinedMode)
608    {
609      appendExtendedRequest(extendedOperation, buffer);
610    }
611
612    final String oid = extendedOperation.getResponseOID();
613    if (oid != null)
614    {
615      final ExtendedOperationHandler<?> extOpHandler = DirectoryServer
616          .getExtendedOperationHandler(oid);
617      if (extOpHandler != null)
618      {
619        String name = extOpHandler.getExtendedOperationName();
620        appendLabelIfNotNull(buffer, "name", name);
621      }
622      appendLabel(buffer, "oid", oid);
623    }
624    appendResultCodeAndMessage(buffer, extendedOperation);
625    logAdditionalLogItems(extendedOperation, buffer);
626    appendEtime(buffer, extendedOperation);
627
628    writer.writeRecord(buffer.toString());
629  }
630
631  @Override
632  public void logModifyDNRequest(final ModifyDNOperation modifyDNOperation)
633  {
634    if (isCombinedMode || !isRequestLoggable(modifyDNOperation))
635    {
636      return;
637    }
638
639    final StringBuilder buffer = new StringBuilder(100);
640    appendHeader(modifyDNOperation, "MODIFYDN", CATEGORY_REQUEST, buffer);
641    appendModifyDNRequest(modifyDNOperation, buffer);
642    writer.writeRecord(buffer.toString());
643  }
644
645  @Override
646  public void logModifyDNResponse(final ModifyDNOperation modifyDNOperation)
647  {
648    if (!isResponseLoggable(modifyDNOperation))
649    {
650      return;
651    }
652
653    final StringBuilder buffer = new StringBuilder(100);
654    appendHeader(modifyDNOperation, "MODIFYDN", CATEGORY_RESPONSE, buffer);
655    if (isCombinedMode)
656    {
657      appendModifyDNRequest(modifyDNOperation, buffer);
658    }
659    appendResultCodeAndMessage(buffer, modifyDNOperation);
660    logAdditionalLogItems(modifyDNOperation, buffer);
661    appendLabelIfNotNull(buffer, "authzDN", modifyDNOperation.getProxiedAuthorizationDN());
662    appendEtime(buffer, modifyDNOperation);
663    writer.writeRecord(buffer.toString());
664  }
665
666  @Override
667  public void logModifyRequest(final ModifyOperation modifyOperation)
668  {
669    if (isCombinedMode || !isRequestLoggable(modifyOperation))
670    {
671      return;
672    }
673
674    final StringBuilder buffer = new StringBuilder(100);
675    appendHeader(modifyOperation, "MODIFY", CATEGORY_REQUEST, buffer);
676    appendModifyRequest(modifyOperation, buffer);
677    writer.writeRecord(buffer.toString());
678  }
679
680  @Override
681  public void logModifyResponse(final ModifyOperation modifyOperation)
682  {
683    if (!isResponseLoggable(modifyOperation))
684    {
685      return;
686    }
687
688    final StringBuilder buffer = new StringBuilder(100);
689    appendHeader(modifyOperation, "MODIFY", CATEGORY_RESPONSE, buffer);
690    if (isCombinedMode)
691    {
692      appendModifyRequest(modifyOperation, buffer);
693    }
694    appendResultCodeAndMessage(buffer, modifyOperation);
695    logAdditionalLogItems(modifyOperation, buffer);
696    appendLabelIfNotNull(buffer, "authzDN", modifyOperation.getProxiedAuthorizationDN());
697    appendEtime(buffer, modifyOperation);
698    writer.writeRecord(buffer.toString());
699  }
700
701  @Override
702  public void logSearchRequest(final SearchOperation searchOperation)
703  {
704    if (isCombinedMode || !isRequestLoggable(searchOperation))
705    {
706      return;
707    }
708
709    final StringBuilder buffer = new StringBuilder(192);
710    appendHeader(searchOperation, "SEARCH", CATEGORY_REQUEST, buffer);
711    appendSearchRequest(searchOperation, buffer);
712    writer.writeRecord(buffer.toString());
713  }
714
715  @Override
716  public void logSearchResultDone(final SearchOperation searchOperation)
717  {
718    if (!isResponseLoggable(searchOperation))
719    {
720      return;
721    }
722
723    final StringBuilder buffer = new StringBuilder(128);
724    appendHeader(searchOperation, "SEARCH", CATEGORY_RESPONSE, buffer);
725    if (isCombinedMode)
726    {
727      appendSearchRequest(searchOperation, buffer);
728    }
729    appendResultCodeAndMessage(buffer, searchOperation);
730    buffer.append(" nentries=").append(searchOperation.getEntriesSent());
731    logAdditionalLogItems(searchOperation, buffer);
732    appendLabelIfNotNull(buffer, "authzDN", searchOperation.getProxiedAuthorizationDN());
733    appendEtime(buffer, searchOperation);
734    writer.writeRecord(buffer.toString());
735  }
736
737  @Override
738  public void logUnbind(final UnbindOperation unbindOperation)
739  {
740    if (!isRequestLoggable(unbindOperation))
741    {
742      return;
743    }
744
745    final StringBuilder buffer = new StringBuilder(100);
746    appendHeader(unbindOperation, "UNBIND", CATEGORY_REQUEST, buffer);
747    if (unbindOperation.isSynchronizationOperation())
748    {
749      buffer.append(" type=synchronization");
750    }
751
752    writer.writeRecord(buffer.toString());
753  }
754
755  @Override
756  protected void close0()
757  {
758    writer.shutdown();
759    TimeThread.removeUserDefinedFormatter(timeStampFormat);
760    if (cfg != null)
761    {
762      cfg.removeFileBasedAccessChangeListener(this);
763    }
764  }
765
766  private void appendAbandonRequest(final AbandonOperation abandonOperation, final StringBuilder buffer)
767  {
768    buffer.append(" idToAbandon=");
769    buffer.append(abandonOperation.getIDToAbandon());
770    appendRequestControls(abandonOperation, buffer);
771    if (abandonOperation.isSynchronizationOperation())
772    {
773      buffer.append(" type=synchronization");
774    }
775  }
776
777  private void appendAddRequest(final AddOperation addOperation, final StringBuilder buffer)
778  {
779    appendLabel(buffer, "dn", addOperation.getRawEntryDN());
780    appendRequestControls(addOperation, buffer);
781    if (addOperation.isSynchronizationOperation())
782    {
783      buffer.append(" type=synchronization");
784    }
785  }
786
787  private void appendBindRequest(final BindOperation bindOperation, final StringBuilder buffer)
788  {
789    final String protocolVersion = bindOperation.getProtocolVersion();
790    if (protocolVersion != null)
791    {
792      buffer.append(" version=");
793      buffer.append(protocolVersion);
794    }
795
796    switch (bindOperation.getAuthenticationType())
797    {
798    case SIMPLE:
799      buffer.append(" type=SIMPLE");
800      break;
801    case SASL:
802      buffer.append(" type=SASL mechanism=").append(bindOperation.getSASLMechanism());
803      break;
804    default:
805      buffer.append(" type=").append(bindOperation.getAuthenticationType());
806      break;
807    }
808
809    appendLabel(buffer, "dn", bindOperation.getRawBindDN());
810    appendRequestControls(bindOperation, buffer);
811    if (bindOperation.isSynchronizationOperation())
812    {
813      buffer.append(" type=synchronization");
814    }
815  }
816
817  private void appendCompareRequest(final CompareOperation compareOperation, final StringBuilder buffer)
818  {
819    appendLabel(buffer, "dn", compareOperation.getRawEntryDN());
820    buffer.append(" attr=");
821    buffer.append(compareOperation.getAttributeType().getNameOrOID());
822    appendRequestControls(compareOperation, buffer);
823    if (compareOperation.isSynchronizationOperation())
824    {
825      buffer.append(" type=synchronization");
826    }
827  }
828
829  private void appendDeleteRequest(final DeleteOperation deleteOperation, final StringBuilder buffer)
830  {
831    appendLabel(buffer, "dn", deleteOperation.getRawEntryDN());
832    appendRequestControls(deleteOperation, buffer);
833    if (deleteOperation.isSynchronizationOperation())
834    {
835      buffer.append(" type=synchronization");
836    }
837  }
838
839  private void appendExtendedRequest(final ExtendedOperation extendedOperation,
840      final StringBuilder buffer)
841  {
842    final String oid = extendedOperation.getRequestOID();
843    final ExtendedOperationHandler<?> extOpHandler = DirectoryServer.getExtendedOperationHandler(oid);
844    if (extOpHandler != null)
845    {
846      final String name = extOpHandler.getExtendedOperationName();
847      appendLabelIfNotNull(buffer, "name", name);
848    }
849    appendLabel(buffer, "oid", oid);
850    appendRequestControls(extendedOperation, buffer);
851    if (extendedOperation.isSynchronizationOperation())
852    {
853      buffer.append(" type=synchronization");
854    }
855  }
856
857  private void appendLabel(final StringBuilder buffer, final String label, final Object obj)
858  {
859    buffer.append(' ').append(label).append("=\"");
860    if (obj != null)
861    {
862      buffer.append(obj);
863    }
864    buffer.append('\"');
865  }
866
867  private void appendLabelIfNotNull(final StringBuilder buffer, final String label, final Object obj)
868  {
869    if (obj != null)
870    {
871      appendLabel(buffer, label, obj);
872    }
873  }
874
875  private void appendResultCodeAndMessage(StringBuilder buffer, Operation operation)
876  {
877    buffer.append(" result=");
878    buffer.append(operation.getResultCode().intValue());
879
880    final LocalizableMessageBuilder msg = operation.getErrorMessage();
881    if (msg != null && msg.length() > 0)
882    {
883      appendLabel(buffer, "message", msg);
884    }
885
886    if (operation.getMaskedResultCode() != null)
887    {
888      buffer.append(" maskedResult=");
889      buffer.append(operation.getMaskedResultCode().intValue());
890    }
891    final LocalizableMessageBuilder maskedMsg = operation.getMaskedErrorMessage();
892    if (maskedMsg != null && maskedMsg.length() > 0)
893    {
894      appendLabel(buffer, "maskedMessage", maskedMsg);
895    }
896  }
897
898  private void appendEtime(final StringBuilder buffer, final Operation operation)
899  {
900    buffer.append(" etime=");
901    // the server can be configured to log processing time as nanos xor millis
902    long etime = operation.getProcessingNanoTime();
903    if (etime <= -1)
904    {
905      // if it is not configured for nanos, then use millis.
906      etime = operation.getProcessingTime();
907    }
908    buffer.append(etime);
909  }
910
911  /** Appends the common log header information to the provided buffer. */
912  private void appendHeader(final Operation operation, final String opType,
913      final String category, final StringBuilder buffer)
914  {
915    buffer.append('[').append(TimeThread.getUserDefinedTime(timeStampFormat)).append("] ");
916    buffer.append(opType);
917    if (!isCombinedMode)
918    {
919      buffer.append(' ').append(category);
920    }
921    buffer.append(" conn=").append(operation.getConnectionID());
922    buffer.append(" op=").append(operation.getOperationID());
923    buffer.append(" msgID=").append(operation.getMessageID());
924    appendTransactionId(operation, buffer);
925  }
926
927  private void appendModifyDNRequest(final ModifyDNOperation modifyDNOperation,
928      final StringBuilder buffer)
929  {
930    appendLabel(buffer, "dn", modifyDNOperation.getRawEntryDN());
931    appendLabel(buffer, "newRDN", modifyDNOperation.getRawNewRDN());
932    appendLabel(buffer, "deleteOldRDN", modifyDNOperation.deleteOldRDN());
933
934    final ByteString newSuperior = modifyDNOperation.getRawNewSuperior();
935    appendLabelIfNotNull(buffer, "newSuperior", newSuperior);
936    appendRequestControls(modifyDNOperation, buffer);
937    if (modifyDNOperation.isSynchronizationOperation())
938    {
939      buffer.append(" type=synchronization");
940    }
941  }
942
943  private void appendModifyRequest(final ModifyOperation modifyOperation, final StringBuilder buffer)
944  {
945    appendLabel(buffer, "dn", modifyOperation.getRawEntryDN());
946    appendRequestControls(modifyOperation, buffer);
947    if (modifyOperation.isSynchronizationOperation())
948    {
949      buffer.append(" type=synchronization");
950    }
951  }
952
953  private void appendTransactionId(Operation operation, final StringBuilder buffer)
954  {
955    // In test context, serverContext may be null
956    if (serverContext != null && serverContext.getCommonAudit().shouldTrustTransactionIds())
957    {
958      String transactionId = getTransactionIdFromControl(operation);
959      if (transactionId != null)
960      {
961        buffer.append(" transactionId=").append(transactionId);
962      }
963    }
964  }
965
966  private String getTransactionIdFromControl(Operation operation)
967  {
968    try
969    {
970      TransactionIdControl control = operation.getRequestControl(TransactionIdControl.DECODER);
971      return control != null ? control.getTransactionId() : null;
972    }
973    catch (DirectoryException e)
974    {
975      logger.error(ERR_COMMON_AUDIT_INVALID_TRANSACTION_ID.get(StaticUtils.stackTraceToSingleLineString(e)));
976    }
977    return null;
978  }
979
980  private void appendRequestControls(final Operation operation, final StringBuilder buffer)
981  {
982    appendControls(buffer, " requestControls=", operation.getRequestControls());
983  }
984
985  private void appendResponseControls(final Operation operation, final StringBuilder buffer)
986  {
987    appendControls(buffer, " responseControls=", operation.getResponseControls());
988  }
989
990  private void appendControls(final StringBuilder buffer, String label, List<Control> controls)
991  {
992    if (includeControlOIDs && !controls.isEmpty())
993    {
994      buffer.append(label);
995      boolean isFirst = true;
996      for (final Control control : controls)
997      {
998        if (!isFirst)
999        {
1000          buffer.append(",");
1001        }
1002        buffer.append(control.getOID());
1003        isFirst = false;
1004      }
1005    }
1006  }
1007
1008  private void appendSearchRequest(final SearchOperation searchOperation, final StringBuilder buffer)
1009  {
1010    appendLabel(buffer, "base", searchOperation.getRawBaseDN());
1011    buffer.append(" scope=");
1012    buffer.append(searchOperation.getScope());
1013    buffer.append(" filter=\"");
1014    searchOperation.getRawFilter().toString(buffer);
1015
1016    final Set<String> attrs = searchOperation.getAttributes();
1017    if (attrs == null || attrs.isEmpty())
1018    {
1019      buffer.append("\" attrs=\"ALL\"");
1020    }
1021    else
1022    {
1023      buffer.append("\" attrs=\"");
1024      Utils.joinAsString(buffer, ",", attrs);
1025      buffer.append("\"");
1026    }
1027    appendRequestControls(searchOperation, buffer);
1028    if (searchOperation.isSynchronizationOperation())
1029    {
1030      buffer.append(" type=synchronization");
1031    }
1032  }
1033
1034  /** Appends additional log items to the provided builder. */
1035  private void logAdditionalLogItems(final Operation operation, final StringBuilder builder)
1036  {
1037    appendResponseControls(operation, builder);
1038    for (final AdditionalLogItem item : operation.getAdditionalLogItems())
1039    {
1040      builder.append(' ');
1041      item.toString(builder);
1042    }
1043  }
1044}