UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleAccessLogger.java

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * docs/licenses/cddl.txt.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Portions Copyright 2010-2023 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.security.auth.x500.X500Principal;

import com.unboundid.directory.sdk.common.api.AccessLogger;
import com.unboundid.directory.sdk.common.api.DiskSpaceConsumer;
import com.unboundid.directory.sdk.common.config.AccessLoggerConfig;
import com.unboundid.directory.sdk.common.operation.AbandonRequest;
import com.unboundid.directory.sdk.common.operation.AddRequest;
import com.unboundid.directory.sdk.common.operation.AddResult;
import com.unboundid.directory.sdk.common.operation.BindResult;
import com.unboundid.directory.sdk.common.operation.CompareRequest;
import com.unboundid.directory.sdk.common.operation.CompareResult;
import com.unboundid.directory.sdk.common.operation.DeleteRequest;
import com.unboundid.directory.sdk.common.operation.DeleteResult;
import com.unboundid.directory.sdk.common.operation.ExtendedRequest;
import com.unboundid.directory.sdk.common.operation.ExtendedResult;
import com.unboundid.directory.sdk.common.operation.GenericResult;
import com.unboundid.directory.sdk.common.operation.ModifyRequest;
import com.unboundid.directory.sdk.common.operation.ModifyResult;
import com.unboundid.directory.sdk.common.operation.ModifyDNRequest;
import com.unboundid.directory.sdk.common.operation.ModifyDNResult;
import com.unboundid.directory.sdk.common.operation.SASLBindRequest;
import com.unboundid.directory.sdk.common.operation.SearchRequest;
import com.unboundid.directory.sdk.common.operation.SearchResult;
import com.unboundid.directory.sdk.common.operation.SimpleBindRequest;
import com.unboundid.directory.sdk.common.operation.UnbindRequest;
import com.unboundid.directory.sdk.common.types.AssuredReplicationRequirements;
import com.unboundid.directory.sdk.common.types.AssuredReplicationResult;
import com.unboundid.directory.sdk.common.types.ClientContext;
import com.unboundid.directory.sdk.common.types.CompletedOperationContext;
import com.unboundid.directory.sdk.common.types.CompletedSearchOperationContext;
import com.unboundid.directory.sdk.common.types.DisconnectReason;
import com.unboundid.directory.sdk.common.types.Entry;
import com.unboundid.directory.sdk.common.types.ForwardTarget;
import com.unboundid.directory.sdk.common.types.OperationContext;
import com.unboundid.directory.sdk.common.types.RegisteredDiskSpaceConsumer;
import com.unboundid.directory.sdk.common.types.ServerContext;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.IntermediateResponse;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.unboundidds.controls.
            AssuredReplicationServerResult;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.FileArgument;



/**
 * This class provides a simple example of an access logger that will append a
 * message to a specified file for any type of client communication.  It takes
 * a single configuration argument:
 * <UL>
 *   <LI>log-file -- The path to the log file that will be written.  This must
 *       be provided.</LI>
 * </UL>
 */
public final class ExampleAccessLogger
       extends AccessLogger
       implements DiskSpaceConsumer
{
  /**
   * The name of the argument that will be used for the argument used to specify
   * the path to the log file.
   */
  private static final String ARG_NAME_LOG_FILE = "log-file";



  // The current general configuration for this access logger.
  private volatile AccessLoggerConfig config;

  // The path to the log file to be written.
  private volatile File logFile;

  // The lock that will be used to synchronize logging activity.
  private final Object loggerLock;

  // The print writer that will be used to actually write the log messages.
  private volatile PrintWriter writer;

  // The registered disk space consumer for this logger.
  private volatile RegisteredDiskSpaceConsumer registeredConsumer;

  // The server context for the server in which this extension is running.
  private ServerContext serverContext;



  /**
   * Creates a new instance of this access logger.  All access logger
   * implementations must include a default constructor, but any initialization
   * should generally be done in the {@code initializeAccessLogger} method.
   */
  public ExampleAccessLogger()
  {
    loggerLock = new Object();
  }



  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override()
  public String getExtensionName()
  {
    return "Example Access Logger";
  }



  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override()
  public String[] getExtensionDescription()
  {
    return new String[]
    {
      "This access logger serves an example that may be used to demonstrate " +
           "the process for creating a third-party access logger.  It will " +
           "write a line to a specified file for each kind of interaction " +
           "with the client.",

      "For simplicity, this example logger does not take care of all tasks " +
           "that would be necessary for production use.  For example, it " +
           "does not include any rotation or retention functionality, so the " +
           "file will grow without bounds.  Further, while it is threadsafe, " +
           "it does so using simple Java synchronized blocks which is a " +
           "simple way to achieve the necessary safety, but does not " +
           "necessarily allow for the highest performance or concurrency.",

      "Because this access logger writes to disk, it also serves as an " +
           "example of a disk space consumer so that the server may track " +
           "available space on the disk containing the log file and " +
           "potentially warn administrators if usable space becomes too low."
    };
  }



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this access logger.  The argument parser may also
   * be updated to define relationships between arguments (e.g., to specify
   * required, exclusive, or dependent argument sets).
   *
   * @param  parser  The argument parser to be updated with the configuration
   *                 arguments which may be used by this access logger.
   *
   * @throws  ArgumentException  If a problem is encountered while updating the
   *                             provided argument parser.
   */
  @Override()
  public void defineConfigArguments(final ArgumentParser parser)
         throws ArgumentException
  {
    // Add an argument that allows you to specify the path to the log file.
    Character shortIdentifier = null;
    String    longIdentifier  = ARG_NAME_LOG_FILE;
    boolean   required        = true;
    int       maxOccurrences  = 1;
    String    placeholder     = "{path}";
    String    description     = "The path to the log file to be written.  " +
         "Non-absolute paths will be treated as relative to the server root.";
    boolean   fileMustExist   = false;
    boolean   parentMustExist = true;
    boolean   mustBeFile      = true;
    boolean   mustBeDirectory = false;

    parser.addArgument(new FileArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description, fileMustExist,
         parentMustExist, mustBeFile, mustBeDirectory));
  }



  /**
   * Initializes this access logger.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this access logger.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this access logger.
   *
   * @throws  LDAPException  If a problem occurs while initializing this access
   *                         logger.
   */
  @Override()
  public void initializeAccessLogger(final ServerContext serverContext,
                                     final AccessLoggerConfig config,
                                     final ArgumentParser parser)
         throws LDAPException
  {
    this.serverContext = serverContext;
    this.config        = config;


    // Create the logger and open the log file.
    try
    {
      final FileArgument arg =
           (FileArgument) parser.getNamedArgument(ARG_NAME_LOG_FILE);
      logFile = arg.getValue();
      writer  = new PrintWriter(new FileWriter(logFile, true));
    }
    catch (final Exception e)
    {
      serverContext.debugCaught(e);
      throw new LDAPException(ResultCode.OTHER,
           "Unable to open file " + logFile.getAbsolutePath() +
                " for writing:  " + StaticUtils.getExceptionMessage(e),
           e);
    }


    // Register as a disk space consumer since the log file may potentially
    // consume a significant amount of disk space.
    registeredConsumer = serverContext.registerDiskSpaceConsumer(this);
  }



  /**
   * Indicates whether the configuration contained in the provided argument
   * parser represents a valid configuration for this extension.
   *
   * @param  config               The general configuration for this access
   *                              logger.
   * @param  parser               The argument parser which has been initialized
   *                              with the proposed configuration.
   * @param  unacceptableReasons  A list that can be updated with reasons that
   *                              the proposed configuration is not acceptable.
   *
   * @return  {@code true} if the proposed configuration is acceptable, or
   *          {@code false} if not.
   */
  @Override()
  public boolean isConfigurationAcceptable(final AccessLoggerConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    // The argument parser will handle all of the necessary validation, so
    // we don't need to do anything here.
    return true;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this access
   *                               logger.
   * @param  parser                The argument parser which has been
   *                               initialized with the new configuration.
   * @param  adminActionsRequired  A list that can be updated with information
   *                               about any administrative actions that may be
   *                               required before one or more of the
   *                               configuration changes will be applied.
   * @param  messages              A list that can be updated with information
   *                               about the result of applying the new
   *                               configuration.
   *
   * @return  A result code that provides information about the result of
   *          attempting to apply the configuration change.
   */
  @Override()
  public ResultCode applyConfiguration(final AccessLoggerConfig config,
                                       final ArgumentParser parser,
                                       final List<String> adminActionsRequired,
                                       final List<String> messages)
  {
    this.config = config;

    // Get the path to the log file from the new config.
    final FileArgument arg =
         (FileArgument) parser.getNamedArgument(ARG_NAME_LOG_FILE);
    final File newFile = arg.getValue();

    // If the log file path hasn't changed, then we don't need to do anything.
    if (newFile.equals(logFile))
    {
      return ResultCode.SUCCESS;
    }

    // Create a print writer that can be used to write to the new log file.
    final PrintWriter newWriter;
    try
    {
      newWriter = new PrintWriter(new FileWriter(newFile, true));
    }
    catch (final Exception e)
    {
      serverContext.debugCaught(e);
      messages.add("Unable to open new log file " + newFile.getAbsolutePath() +
           " for writing:  " + StaticUtils.getExceptionMessage(e));
      return ResultCode.OTHER;
    }

    // Swap the new logger into place.
    final PrintWriter oldWriter;
    synchronized (loggerLock)
    {
      oldWriter = writer;
      writer = newWriter;
      logFile = newFile;
    }

    // Close the old logger and return success.
    oldWriter.close();
    return ResultCode.SUCCESS;
  }



  /**
   * Performs any cleanup which may be necessary when this access logger is
   * to be taken out of service.
   */
  @Override()
  public void finalizeAccessLogger()
  {
    synchronized (loggerLock)
    {
      if (registeredConsumer != null)
      {
        serverContext.deregisterDiskSpaceConsumer(registeredConsumer);
        registeredConsumer = null;
      }

      writer.close();
      writer = null;
      logFile = null;
    }
  }



  /**
   * Logs a message indicating that a new connection has been established.
   *
   * @param  clientContext  Information about the client connection that has
   *                        been accepted.
   */
  @Override()
  public void logConnect(final ClientContext clientContext)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" CONNECT conn=");
    buffer.append(clientContext.getConnectionID());

    final InetAddress clientAddress = clientContext.getClientInetAddress();
    if (clientAddress != null)
    {
      buffer.append(" from=\"");
      buffer.append(clientAddress.getHostAddress());
      buffer.append('"');
    }

    final InetAddress serverAddress = clientContext.getServerInetAddress();
    if (serverAddress != null)
    {
      buffer.append(" to=");
      buffer.append(serverAddress.getHostAddress());
      buffer.append('"');
    }

    buffer.append(" protocol=\"");
    buffer.append(clientContext.getProtocol());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message indicating that a connection has been closed.
   *
   * @param  clientContext     Information about the client connection that has
   *                           been closed.
   * @param  disconnectReason  A general reason that the connection has been
   *                           closed.
   * @param  message           A message with additional information about the
   *                           closure.  It may be {@code null} if none is
   *                           available.
   */
  @Override()
  public void logDisconnect(final ClientContext clientContext,
                            final DisconnectReason disconnectReason,
                            final String message)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" DISCONNECT conn=");
    buffer.append(clientContext.getConnectionID());

    buffer.append(" reason=\"");
    buffer.append(disconnectReason.getClosureMessage());
    buffer.append('"');

    if (message != null)
    {
      buffer.append(" message=\"");
      buffer.append(message);
      buffer.append('"');
    }

    write(buffer);
  }



  /**
   * Logs a message about security negotiation performed by a client.
   *
   * @param  clientContext  Information about the client connection on which
   *                        the negotiation was completed.
   * @param  protocol       The security protocol selected by the negotiation.
   *                        It may be {@code null} if no protocol is available.
   * @param  cipher         The cipher suite selected by the negotiation.  It
   *                        may be {@code null} if no cipher is available.
   * @param  properties     A set of additional properties that may be included
   *                        in the log message.  It may be {@code null} or empty
   *                        if no additional properties are needed.
   */
  @Override()
  public void logSecurityNegotiation(final ClientContext clientContext,
                                     final String protocol, final String cipher,
                                     final Map<String,String> properties)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" SECURITY-NEGOTIATION conn=");
    buffer.append(clientContext.getConnectionID());

    if (protocol != null)
    {
      buffer.append(" protocol=\"");
      buffer.append(protocol);
      buffer.append('"');
    }

    if (cipher != null)
    {
      buffer.append(" cipher=\"");
      buffer.append(cipher);
      buffer.append('"');
    }

    if ((properties != null) && (! properties.isEmpty()))
    {
      for (final Map.Entry<String,String> e : properties.entrySet())
      {
        buffer.append(e.getKey());
        buffer.append("=\"");
        buffer.append(e.getValue());
        buffer.append('"');
      }
    }

    write(buffer);
  }



  /**
   * Logs a message about a certificate chain presented by a client.
   *
   * @param  clientContext  Information about the client that presented the
   *                        certificate chain.
   * @param  certChain      The certificate chain presented by the client.
   * @param  authDN         The DN of the user as whom the client was
   *                        automatically authenticated, or {@code null} if the
   *                        client was not automatically authenticated.
   */
  @Override()
  public void logClientCertificateChain(final ClientContext clientContext,
                                        final Certificate[] certChain,
                                        final String authDN)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" CERTIFICATE conn=");
    buffer.append(clientContext.getConnectionID());

    if (certChain.length > 0)
    {
      final Certificate cert = certChain[0];
      if (cert instanceof X509Certificate)
      {
        final X509Certificate c = (X509Certificate) cert;
        final String subject =
             c.getSubjectX500Principal().getName(X500Principal.RFC2253);
        buffer.append(" subject=\"");
        buffer.append(subject);
        buffer.append('"');
      }
    }

    if (authDN != null)
    {
      buffer.append(" authDN=\"");
      buffer.append(authDN);
      buffer.append('"');
    }

    write(buffer);
  }



  /**
   * Logs a message about an abandon request received from a client.
   *
   * @param  opContext  The operation context for the abandon operation.
   * @param  request    The abandon request that was received.
   */
  @Override()
  public void logAbandonRequest(final OperationContext opContext,
                                final AbandonRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" ABANDON REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" idToAbandon=");
    buffer.append(request.getIDToAbandon());

    write(buffer);
  }



  /**
   * Logs a message about an abandon request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the abandon operation.
   * @param  request    The abandon request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logAbandonForward(final OperationContext opContext,
                                final AbandonRequest request,
                                final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about the result of processing an abandon request.
   *
   * @param  opContext  The operation context for the abandon operation.
   * @param  request    The abandon request that was received.
   * @param  result     The result of processing the abandon request.
   */
  @Override()
  public void logAbandonResult(final CompletedOperationContext opContext,
                               final AbandonRequest request,
                               final GenericResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about an add request received from a client.
   *
   * @param  opContext  The operation context for the add operation.
   * @param  request    The add request that was received.
   */
  @Override()
  public void logAddRequest(final OperationContext opContext,
                            final AddRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" ADD REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" dn=\"");
    buffer.append(request.getEntry().getDN());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about an add request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the add operation.
   * @param  request    The add request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logAddForward(final OperationContext opContext,
                            final AddRequest request,
                            final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward an
   * add request to another server.
   *
   * @param  opContext  The operation context for the add operation.
   * @param  request    The add request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logAddForwardFailure(final OperationContext opContext,
                                   final AddRequest request,
                                   final ForwardTarget target,
                                   final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing an add request.
   *
   * @param  opContext  The operation context for the add operation.
   * @param  request    The add request that was received.
   * @param  result     The result of processing the add request.
   */
  @Override()
  public void logAddResponse(final CompletedOperationContext opContext,
                             final AddRequest request,
                             final AddResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about the result of replication assurance processing for an
   * add operation.
   *
   * @param  opContext        The operation context for the add operation.
   * @param  request          The add request that was received.
   * @param  result           The result of processing the add request.
   * @param  assuranceResult  The replication assurance processing result.
   */
  @Override()
  public void logAddAssuranceCompleted(
                   final CompletedOperationContext opContext,
                   final AddRequest request, final AddResult result,
                   final AssuredReplicationResult assuranceResult)
  {
    writeAssuranceResult(opContext, result.getAssuredReplicationRequirements(),
         assuranceResult);
  }



  /**
   * Logs a message about a simple bind request received from a client.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   */
  @Override()
  public void logBindRequest(final OperationContext opContext,
                             final SimpleBindRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" BIND REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" authType=SIMPLE dn=\"");
    buffer.append(" dn=\"");
    buffer.append(request.getDN());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a simple bind request that will be forwarded to
   * another server.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logBindForward(final OperationContext opContext,
                                final SimpleBindRequest request,
                                final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * simple bind request to another server.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logBindForwardFailure(final OperationContext opContext,
                                    final SimpleBindRequest request,
                                    final ForwardTarget target,
                                    final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing a simple bind request.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   * @param  result     The result of processing the bind request.
   */
  @Override()
  public void logBindResponse(final CompletedOperationContext opContext,
                              final SimpleBindRequest request,
                              final BindResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about a SASL bind request received from a client.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   */
  @Override()
  public void logBindRequest(final OperationContext opContext,
                             final SASLBindRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" BIND REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" authType=SASL mechanism=\"");
    buffer.append(request.getSASLMechanism());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a SASL bind request that will be forwarded to
   * another server.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logBindForward(final OperationContext opContext,
                                final SASLBindRequest request,
                                final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * SASL bind request to another server.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logBindForwardFailure(final OperationContext opContext,
                                    final SASLBindRequest request,
                                    final ForwardTarget target,
                                    final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing a SASL bind request.
   *
   * @param  opContext  The operation context for the bind operation.
   * @param  request    The bind request that was received.
   * @param  result     The result of processing the bind request.
   */
  @Override()
  public void logBindResponse(final CompletedOperationContext opContext,
                              final SASLBindRequest request,
                              final BindResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about a compare request received from a client.
   *
   * @param  opContext  The operation context for the compare operation.
   * @param  request    The compare request that was received.
   */
  @Override()
  public void logCompareRequest(final OperationContext opContext,
                                final CompareRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" COMPARE REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" dn=\"");
    buffer.append(request.getDN());
    buffer.append('"');

    buffer.append(" dn=\"");
    buffer.append(request.getDN());
    buffer.append("\" attribute=\"");
    buffer.append(request.getAttributeType());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a compare request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the compare operation.
   * @param  request    The compare request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logCompareForward(final OperationContext opContext,
                                final CompareRequest request,
                                final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * compare request to another server.
   *
   * @param  opContext  The operation context for the compare operation.
   * @param  request    The compare request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logCompareForwardFailure(final OperationContext opContext,
                                       final CompareRequest request,
                                       final ForwardTarget target,
                                       final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing a compare request.
   *
   * @param  opContext  The operation context for the compare operation.
   * @param  request    The compare request that was received.
   * @param  result     The result of processing the compare request.
   */
  @Override()
  public void logCompareResponse(final CompletedOperationContext opContext,
                                 final CompareRequest request,
                                 final CompareResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about a delete request received from a client.
   *
   * @param  opContext  The operation context for the delete operation.
   * @param  request    The delete request that was received.
   */
  @Override()
  public void logDeleteRequest(final OperationContext opContext,
                               final DeleteRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" DELETE REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" dn=\"");
    buffer.append(request.getDN());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a delete request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the delete operation.
   * @param  request    The delete request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logDeleteForward(final OperationContext opContext,
                               final DeleteRequest request,
                               final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * delete request to another server.
   *
   * @param  opContext  The operation context for the delete operation.
   * @param  request    The delete request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logDeleteForwardFailure(final OperationContext opContext,
                                      final DeleteRequest request,
                                      final ForwardTarget target,
                                      final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing a delete request.
   *
   * @param  opContext  The operation context for the delete operation.
   * @param  request    The delete request that was received.
   * @param  result     The result of processing the delete request.
   */
  @Override()
  public void logDeleteResponse(final CompletedOperationContext opContext,
                                final DeleteRequest request,
                                final DeleteResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about the result of replication assurance processing for a
   * delete operation.
   *
   * @param  opContext        The operation context for the delete operation.
   * @param  request          The delete request that was received.
   * @param  result           The result of processing the delete request.
   * @param  assuranceResult  The replication assurance processing result.
   */
  @Override()
  public void logDeleteAssuranceCompleted(
                   final CompletedOperationContext opContext,
                   final DeleteRequest request, final DeleteResult result,
                   final AssuredReplicationResult assuranceResult)
  {
    writeAssuranceResult(opContext, result.getAssuredReplicationRequirements(),
         assuranceResult);
  }



  /**
   * Logs a message about an extended request received from a client.
   *
   * @param  opContext  The operation context for the extended operation.
   * @param  request    The extended request that was received.
   */
  @Override()
  public void logExtendedRequest(final OperationContext opContext,
                                 final ExtendedRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" EXTENDED REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" oid=\"");
    buffer.append(request.getRequestOID());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about an extended request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the extended operation.
   * @param  request    The extended request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logExtendedForward(final OperationContext opContext,
                                 final ExtendedRequest request,
                                 final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward an
   * extended request to another server.
   *
   * @param  opContext  The operation context for the extended operation.
   * @param  request    The extended request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logExtendedForwardFailure(final OperationContext opContext,
                                        final ExtendedRequest request,
                                        final ForwardTarget target,
                                        final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing an extended request.
   *
   * @param  opContext  The operation context for the extended operation.
   * @param  request    The extended request that was received.
   * @param  result     The result of processing the extended request.
   */
  @Override()
  public void logExtendedResponse(final CompletedOperationContext opContext,
                                  final ExtendedRequest request,
                                  final ExtendedResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about a modify request received from a client.
   *
   * @param  opContext  The operation context for the modify operation.
   * @param  request    The modify request that was received.
   */
  @Override()
  public void logModifyRequest(final OperationContext opContext,
                               final ModifyRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" MODIFY REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" dn=\"");
    buffer.append(request.getDN());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a modify request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the modify operation.
   * @param  request    The modify request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logModifyForward(final OperationContext opContext,
                               final ModifyRequest request,
                               final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * modify request to another server.
   *
   * @param  opContext  The operation context for the modify operation.
   * @param  request    The modify request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logModifyForwardFailure(final OperationContext opContext,
                                      final ModifyRequest request,
                                      final ForwardTarget target,
                                      final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing a modify request.
   *
   * @param  opContext  The operation context for the modify operation.
   * @param  request    The modify request that was received.
   * @param  result     The result of processing the modify request.
   */
  @Override()
  public void logModifyResponse(final CompletedOperationContext opContext,
                                final ModifyRequest request,
                                final ModifyResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about the result of replication assurance processing for a
   * modify operation.
   *
   * @param  opContext        The operation context for the modify operation.
   * @param  request          The modify request that was received.
   * @param  result           The result of processing the modify request.
   * @param  assuranceResult  The replication assurance processing result.
   */
  @Override()
  public void logModifyAssuranceCompleted(
                   final CompletedOperationContext opContext,
                   final ModifyRequest request, final ModifyResult result,
                   final AssuredReplicationResult assuranceResult)
  {
    writeAssuranceResult(opContext, result.getAssuredReplicationRequirements(),
         assuranceResult);
  }



  /**
   * Logs a message about a modify DN request received from a client.
   *
   * @param  opContext  The operation context for the modify DN operation.
   * @param  request    The modify DN request that was received.
   */
  @Override()
  public void logModifyDNRequest(final OperationContext opContext,
                                 final ModifyDNRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" MODIFY_DN REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" dn=\"");
    buffer.append(request.getDN());
    buffer.append("\" newRDN=\"");
    buffer.append(request.getNewRDN());
    buffer.append("\" deleteOldRDN=");
    buffer.append(request.deleteOldRDN());

    final String newSuperior = request.getNewSuperiorDN();
    if (newSuperior != null)
    {
      buffer.append(" newSuperior=\"");
      buffer.append(newSuperior);
      buffer.append('"');
    }

    write(buffer);
  }



  /**
   * Logs a message about a modify DN request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the modify DN operation.
   * @param  request    The modify DN request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logModifyDNForward(final OperationContext opContext,
                                 final ModifyDNRequest request,
                                 final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * modify DN request to another server.
   *
   * @param  opContext  The operation context for the modify DN operation.
   * @param  request    The modify DN request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logModifyDNForwardFailure(final OperationContext opContext,
                                        final ModifyDNRequest request,
                                        final ForwardTarget target,
                                        final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about the result of processing a modify DN request.
   *
   * @param  opContext  The operation context for the modify DN operation.
   * @param  request    The modify DN request that was received.
   * @param  result     The result of processing the modify DN request.
   */
  @Override()
  public void logModifyDNResponse(final CompletedOperationContext opContext,
                                  final ModifyDNRequest request,
                                  final ModifyDNResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about the result of replication assurance processing for a
   * modify DN operation.
   *
   * @param  opContext        The operation context for the modify DN operation.
   * @param  request          The modify DN request that was received.
   * @param  result           The result of processing the modify DN request.
   * @param  assuranceResult  The replication assurance processing result.
   */
  @Override()
  public void logModifyDNAssuranceCompleted(
                   final CompletedOperationContext opContext,
                   final ModifyDNRequest request, final ModifyDNResult result,
                   final AssuredReplicationResult assuranceResult)
  {
    writeAssuranceResult(opContext, result.getAssuredReplicationRequirements(),
         assuranceResult);
  }



  /**
   * Logs a message about a search request received from a client.
   *
   * @param  opContext  The operation context for the search operation.
   * @param  request    The search request that was received.
   */
  @Override()
  public void logSearchRequest(final OperationContext opContext,
                               final SearchRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" SEARCH REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" base=\"");
    buffer.append(request.getBaseDN());
    buffer.append("\" scope=");
    buffer.append(request.getScope().intValue());
    buffer.append(" filter=\"");
    buffer.append(request.getFilter().toString());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a search request that will be forwarded to another
   * server.
   *
   * @param  opContext  The operation context for the search operation.
   * @param  request    The search request that was received.
   * @param  target     Information about the server to which the request will
   *                    be forwarded.
   */
  @Override()
  public void logSearchForward(final OperationContext opContext,
                               final SearchRequest request,
                               final ForwardTarget target)
  {
    writeForward(opContext, target);
  }



  /**
   * Logs a message about a failure encountered while attempting to forward a
   * search request to another server.
   *
   * @param  opContext  The operation context for the search operation.
   * @param  request    The search request that was received.
   * @param  target     Information about the server to which the request was
   *                    forwarded.
   * @param  failure    The exception that was received when attempting to
   *                    forward the request.
   */
  @Override()
  public void logSearchForwardFailure(final OperationContext opContext,
                                      final SearchRequest request,
                                      final ForwardTarget target,
                                      final LDAPException failure)
  {
    writeForwardFailure(opContext, target, failure);
  }



  /**
   * Logs a message about a search result entry that was returned to the client.
   *
   * @param  opContext  The operation context for the search operation.
   * @param  request    The search request that was received.
   * @param  entry      The entry that was returned.
   * @param  controls   The set of controls included with the entry, or an empty
   *                    list if there were none.
   */
  @Override()
  public void logSearchResultEntry(final OperationContext opContext,
                                   final SearchRequest request,
                                   final Entry entry,
                                   final List<Control> controls)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" SEARCH ENTRY conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" dn=\"");
    buffer.append(entry.getDN());
    buffer.append('"');

    write(buffer);
  }



  /**
   * Logs a message about a search result reference that was returned to the
   * client.
   *
   * @param  opContext     The operation context for the search operation.
   * @param  request       The search request that was received.
   * @param  referralURLs  The referral URLs for the reference that was
   *                       returned.
   * @param  controls      The set of controls included with the reference, or
   *                       an empty list if there were none.
   */
  @Override()
  public void logSearchResultReference(final OperationContext opContext,
                                       final SearchRequest request,
                                       final List<String> referralURLs,
                                       final List<Control> controls)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" SEARCH REFERENCE conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" referralURLs={");
    final Iterator<String> iterator = referralURLs.iterator();
    while (! iterator.hasNext())
    {
      buffer.append('"');
      buffer.append(iterator.next());
      buffer.append('"');

      if (iterator.hasNext())
      {
        buffer.append(", ");
      }
    }
    buffer.append('}');

    write(buffer);
  }



  /**
   * Logs a message about the result of processing a search request.
   *
   * @param  opContext  The operation context for the search operation.
   * @param  request    The search request that was received.
   * @param  result     The result of processing the search request.
   */
  @Override()
  public void logSearchResultDone(
                   final CompletedSearchOperationContext opContext,
                   final SearchRequest request, final SearchResult result)
  {
    writeResult(opContext, result);
  }



  /**
   * Logs a message about an unbind request received from a client.
   *
   * @param  opContext  The operation context for the unbind operation.
   * @param  request    The unbind request that was received.
   */
  @Override()
  public void logUnbindRequest(final OperationContext opContext,
                               final UnbindRequest request)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" UNBIND REQUEST conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    write(buffer);
  }



  /**
   * Logs a message about an intermediate response that was returned to the
   * client.
   *
   * @param  opContext             The operation context for the associated
   *                               operation.
   * @param  intermediateResponse  The intermediate response that was returned.
   */
  @Override()
  public void logIntermediateResponse(final OperationContext opContext,
                   final IntermediateResponse intermediateResponse)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(" INTERMEDIATE RESPONSE conn=");
    buffer.append(opContext.getConnectionID());

    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    final String oid = intermediateResponse.getOID();
    if (oid != null)
    {
      buffer.append(" oid=\"");
      buffer.append(oid);
      buffer.append('"');
    }

    final String name = intermediateResponse.getIntermediateResponseName();
    if ((name != null) && (! name.equals(oid)))
    {
      buffer.append(" name=\"");
      buffer.append(name);
      buffer.append('"');
    }

    final String valueStr = intermediateResponse.valueToString();
    if (valueStr != null)
    {
      buffer.append(" value=\"");
      buffer.append(valueStr);
      buffer.append('"');
    }

    write(buffer);
  }



  /**
   * Logs information about an operation to be forwarded.
   *
   * @param  opContext  The operation context for the operation.
   * @param  target     The forward target for the operation.
   */
  private void writeForward(final OperationContext opContext,
                            final ForwardTarget target)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(' ');
    buffer.append(opContext.getOperationType().getOperationName());
    buffer.append(" FORWARD conn=");
    buffer.append(opContext.getConnectionID());
    buffer.append(" op=");
    buffer.append(opContext.getOperationID());
    buffer.append(" targetAddress=");
    buffer.append(target.getForwardTargetAddress());
    buffer.append(" targetPort=");
    buffer.append(target.getForwardTargetPort());

    write(buffer);
  }



  /**
   * Logs information about a failed forward attempt.
   *
   * @param  opContext  The operation context for the operation.
   * @param  target     The forward target for the operation.
   * @param  failure    The exception caught when trying to forward the
   *                    operation.
   */
  private void writeForwardFailure(final OperationContext opContext,
                                   final ForwardTarget target,
                                   final LDAPException failure)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(' ');
    buffer.append(opContext.getOperationType().getOperationName());
    buffer.append(" FORWARD FAILURE conn=");
    buffer.append(opContext.getConnectionID());
    buffer.append(" op=");
    buffer.append(opContext.getOperationID());
    buffer.append(" targetAddress=");
    buffer.append(target.getForwardTargetAddress());
    buffer.append(" targetPort=");
    buffer.append(target.getForwardTargetPort());
    buffer.append(" resultCode=");
    buffer.append(failure.getResultCode());

    final String diagnosticMessage = failure.getDiagnosticMessage();
    if (diagnosticMessage != null)
    {
      buffer.append(" diagnosticMessage=\"");
      buffer.append(diagnosticMessage);
      buffer.append('"');
    }

    final String matchedDN = failure.getMatchedDN();
    if (matchedDN != null)
    {
      buffer.append(" matchedDN=\"");
      buffer.append(matchedDN);
      buffer.append('"');
    }

    final String[] referralURLs = failure.getReferralURLs();
    if ((referralURLs != null) && (referralURLs.length > 0))
    {
      buffer.append(" referralURLs={");
      for (int i=0; i < referralURLs.length; i++)
      {
        if (i > 0)
        {
          buffer.append(", ");
        }

        buffer.append('"');
        buffer.append(referralURLs[i]);
        buffer.append('"');
      }
      buffer.append('}');
    }

    write(buffer);
  }



  /**
   * Logs information about the result of the provided operation.
   *
   * @param  opContext  The operation context for the operation.
   * @param  result     The result to be logged.
   */
  private void writeResult(final CompletedOperationContext opContext,
                           final GenericResult result)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(' ');
    buffer.append(opContext.getOperationType().getOperationName());
    buffer.append(" RESULT conn=");
    buffer.append(opContext.getConnectionID());
    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" resultCode=");
    buffer.append(result.getResultCode());

    final String diagnosticMessage = result.getDiagnosticMessage();
    if (diagnosticMessage != null)
    {
      buffer.append(" diagnosticMessage=\"");
      buffer.append(diagnosticMessage);
      buffer.append('"');
    }

    final String matchedDN = result.getMatchedDN();
    if (matchedDN != null)
    {
      buffer.append(" matchedDN=\"");
      buffer.append(matchedDN);
      buffer.append('"');
    }

    final List<String> referralURLs = result.getReferralURLs();
    if ((referralURLs != null) && (! referralURLs.isEmpty()))
    {
      buffer.append(" referralURLs={");
      final Iterator<String> iterator = referralURLs.iterator();
      while (! iterator.hasNext())
      {
        buffer.append('"');
        buffer.append(iterator.next());
        buffer.append('"');

        if (iterator.hasNext())
        {
          buffer.append(", ");
        }
      }
      buffer.append('}');
    }

    final String additionalMessage = result.getAdditionalLogMessage();
    if (additionalMessage != null)
    {
      buffer.append(" additionalLogMessage=\"");
      buffer.append(additionalMessage);
      buffer.append('"');
    }

    buffer.append(" elapsedTimeMillis=");
    buffer.append(opContext.getProcessingTimeMillis());

    if (opContext instanceof CompletedSearchOperationContext)
    {
      final CompletedSearchOperationContext c =
           (CompletedSearchOperationContext) opContext;
      buffer.append(" entriesReturned=");
      buffer.append(c.getEntryCount());
      buffer.append(" referencesReturned=");
      buffer.append(c.getReferenceCount());
    }

    if (result instanceof ExtendedResult)
    {
      final ExtendedResult r = (ExtendedResult) result;
      final String resultOID = r.getResultOID();
      if (resultOID != null)
      {
        buffer.append(" resultOID=\"");
        buffer.append(resultOID);
        buffer.append('"');
      }
    }

    write(buffer);
  }



  /**
   * Logs information about the result of assurance processing for the provided
   * operation.
   *
   * @param  opContext              The operation context for the operation.
   * @param  assuranceRequirements  The assurance requirements for the
   *                                operation.
   * @param  assuranceResult        The assurance result for the operation.
   */
  private void writeAssuranceResult(final CompletedOperationContext opContext,
                    final AssuredReplicationRequirements assuranceRequirements,
                    final AssuredReplicationResult assuranceResult)
  {
    final StringBuilder buffer = new StringBuilder();

    buffer.append(new Date().toString());

    buffer.append(' ');
    buffer.append(opContext.getOperationType().getOperationName());
    buffer.append(" ASSURANCE-RESULT conn=");
    buffer.append(opContext.getConnectionID());
    buffer.append(" op=");
    buffer.append(opContext.getOperationID());

    buffer.append(" localLevel=");
    buffer.append(assuranceRequirements.getLocalLevel().name());
    buffer.append(" localAssuranceSatisfied=");
    buffer.append(assuranceResult.isLocalAssuranceSatisfied());

    buffer.append(" remoteLevel=");
    buffer.append(assuranceRequirements.getRemoteLevel().name());
    buffer.append(" remoteAssuranceSatisfied=");
    buffer.append(assuranceResult.isRemoteAssuranceSatisfied());

    final Collection<AssuredReplicationServerResult> serverResults =
         assuranceResult.getServerResults();
    if ((serverResults != null) && (! serverResults.isEmpty()))
    {
      buffer.append(" serverAssuranceResults='");

      final Iterator<AssuredReplicationServerResult> iterator =
           serverResults.iterator();
      while (iterator.hasNext())
      {
        iterator.next().toString(buffer);
        if (iterator.hasNext())
        {
          buffer.append(", ");
        }
      }
      buffer.append('\'');
    }

    write(buffer);
  }



  /**
   * Writes the contents of the provided buffer to the logger.
   *
   * @param  buffer  The buffer containing the data to write.
   */
  private void write(final StringBuilder buffer)
  {
    synchronized (loggerLock)
    {
      if (writer == null)
      {
        // This should only happen if the logger has been shut down.
        return;
      }

      writer.println(buffer.toString());
    }
  }



  /**
   * Retrieves a map containing examples of configurations that may be used for
   * this extension.  The map key should be a list of sample arguments, and the
   * corresponding value should be a description of the behavior that will be
   * exhibited by the extension when used with that configuration.
   *
   * @return  A map containing examples of configurations that may be used for
   *          this extension.  It may be {@code null} or empty if there should
   *          not be any example argument sets.
   */
  @Override()
  public Map<List<String>,String> getExamplesArgumentSets()
  {
    final LinkedHashMap<List<String>,String> exampleMap =
         new LinkedHashMap<List<String>,String>(1);

    exampleMap.put(
         Arrays.asList(ARG_NAME_LOG_FILE + "=logs/example-access.log"),
         "Write access log messages to the logs/example-access.log file " +
              "below the server root.");

    return exampleMap;
  }



  /**
   * Retrieves the name that should be used to identify this disk space
   * consumer.
   *
   * @return  The name that should be used to identify this disk space consumer.
   */
  public String getDiskSpaceConsumerName()
  {
    return "Example Access Logger " + config.getConfigObjectName();
  }



  /**
   * Retrieves a list of filesystem paths in which this disk space consumer may
   * store files which may consume a significant amount of space.  It is
   * generally recommended that the paths be directories, but they may also be
   * individual files.
   *
   * @return  A list of filesystem paths in which this disk space consumer may
   *          store files which may consume a significant amount of space.
   */
  public List<File> getDiskSpaceConsumerPaths()
  {
    return Arrays.asList(logFile);
  }
}