mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Jean-Noel Rouvignac
03.33.2013 834a8ae734c7af26826962d52e1aace2bd7bbef3
OPENDJ-879 (CR-1642) Add HTTP access log



Implemented the HTTP access logger by taking inspiration from other loggers.
Configured the new logger everywhere other loggers are configured.
Logged the HTTP request in several places to cover all cases (happy paths, errors, etc.).



config.ldif, 02-config.ldif, HTTPAccessLogPublisherConfiguration.xml, FileBasedHTTPAccessLogPublisherConfiguration.xml, HTTPAccessLogPublisherCfgDefn.properties, FileBasedHTTPAccessLogPublisherCfgDefn.properties: ADDED
Added 2 new objectClasses HTTPAccessLogPublisherConfiguration and FileBasedHTTPAccessLogPublisherConfiguration.

config.properties:
Added new error messages for the HTTP access logger.

HTTPAccessLogger.java, HTTPAccessLogPublisher.java, TextHTTPAccessLogPublisher.java, HTTPRequestInfo.java: ADDED
HTTPRequestInfo.log() prevents double logging.

CollectClientConnectionsFilter.java:
Logged the request info when HttpServletResponse.setStatus(), sendAuthenticationFailure() and onFailure() are called.
Pushed more data to the HTTPRequestContext + switched to use the more specific HttpServletRequest/HttpServletResponse

SdkConnectionAdapter.java:
Logged the request info when close() is called.

LoggerConfigManager.java, TestCaseUtils.java:
Configured the HTTP access logger.

InProcessServerController.java: TO BE REMOVED (by Matt on the native packaging branch)



Sample log:
localhost bjensen [03/May/2013:10:14:54 +0200] "GET /users/_queryFilter=true&_prettyPrint=true HTTP/1.1" 500 "curl/7.27.0"
localhost bjensen [03/May/2013:10:15:05 +0200] "GET /users/_queryFilter=true&_prettyPrint=true HTTP/1.1" 200 "curl/7.27.0"
localhost [03/May/2013:10:15:14 +0200] "GET /users/_queryFilter=true&_prettyPrint=true HTTP/1.1" 200 "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:20.0) Gecko/20100101 Firefox/20.0"
localhost [03/May/2013:10:16:40 +0200] "GET /users/_queryFilter=true&_prettyPrint=true HTTP/1.1" 401 "curl/7.27.0"
localhost [03/May/2013:10:16:50 +0200] "GET /users/_queryFilter=true&_prettyPrint=true HTTP/1.1" 200 "curl/7.27.0"
localhost [03/May/2013:10:16:51 +0200] "GET /favicon.ico/null HTTP/1.1" 404 "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:20.0) Gecko/20100101 Firefox/20.0"
localhost [03/May/2013:10:17:10 +0200] "GET /users/_queryFilter=true&_prettyPrint=true HTTP/1.1" 200 "curl/7.27.0"
8 files added
7 files modified
1465 ■■■■■ changed files
opends/resource/config/config.ldif 15 ●●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif 22 ●●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/FileBasedHTTPAccessLogPublisherConfiguration.xml 316 ●●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/HTTPAccessLogPublisherConfiguration.xml 59 ●●●●● patch | view | raw | blame | history
opends/src/admin/messages/FileBasedHTTPAccessLogPublisherCfgDefn.properties 26 ●●●●● patch | view | raw | blame | history
opends/src/admin/messages/HTTPAccessLogPublisherCfgDefn.properties 6 ●●●●● patch | view | raw | blame | history
opends/src/messages/messages/config.properties 9 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/api/HTTPAccessLogPublisher.java 101 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/LoggerConfigManager.java 26 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/loggers/HTTPAccessLogger.java 148 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/loggers/HTTPRequestInfo.java 177 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/loggers/TextHTTPAccessLogPublisher.java 409 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java 96 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java 17 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java 38 ●●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif
@@ -727,6 +727,21 @@
ds-cfg-rotation-policy: cn=Size Limit Rotation Policy,cn=Log Rotation Policies,cn=config
ds-cfg-retention-policy: cn=File Count Retention Policy,cn=Log Retention Policies,cn=config
dn: cn=File-Based HTTP Access Logger,cn=Loggers,cn=config
objectClass: top
objectClass: ds-cfg-log-publisher
objectClass: ds-cfg-http-access-log-publisher
objectClass: ds-cfg-file-based-http-access-log-publisher
cn: File-Based HTTP Access Logger
ds-cfg-java-class: org.opends.server.loggers.TextHTTPAccessLogPublisher
ds-cfg-enabled: true
ds-cfg-log-file: logs/http-access
ds-cfg-log-file-permissions: 640
ds-cfg-asynchronous: true
ds-cfg-rotation-policy: cn=24 Hours Time Limit Rotation Policy,cn=Log Rotation Policies,cn=config
ds-cfg-rotation-policy: cn=Size Limit Rotation Policy,cn=Log Rotation Policies,cn=config
ds-cfg-retention-policy: cn=File Count Retention Policy,cn=Log Retention Policies,cn=config
dn: cn=File-Based Audit Logger,cn=Loggers,cn=config
objectClass: top
objectClass: ds-cfg-log-publisher
opends/resource/schema/02-config.ldif
@@ -3937,6 +3937,11 @@
        ds-cfg-suppress-internal-operations $
        ds-cfg-suppress-synchronization-operations )
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.16
  NAME 'ds-cfg-http-access-log-publisher'
  SUP ds-cfg-log-publisher
  STRUCTURAL
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.23
  NAME 'ds-cfg-error-log-publisher'
  SUP ds-cfg-log-publisher
@@ -3974,6 +3979,23 @@
        ds-cfg-log-record-time-format $
        ds-cfg-log-control-oids )
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.17
  NAME 'ds-cfg-file-based-http-access-log-publisher'
  SUP ds-cfg-http-access-log-publisher
  STRUCTURAL
  MUST ( ds-cfg-log-file $
         ds-cfg-asynchronous $
         ds-cfg-log-file-permissions )
  MAY ( ds-cfg-rotation-policy $
        ds-cfg-rotation-action $
        ds-cfg-retention-policy $
        ds-cfg-time-interval $
        ds-cfg-buffer-size $
        ds-cfg-auto-flush $
        ds-cfg-append $
        ds-cfg-queue-size $
        ds-cfg-log-record-time-format )
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.26
  NAME 'ds-cfg-file-based-debug-log-publisher'
  SUP ds-cfg-debug-log-publisher
opends/src/admin/defn/org/opends/server/admin/std/FileBasedHTTPAccessLogPublisherConfiguration.xml
New file
@@ -0,0 +1,316 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
  ! 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
  ! trunk/opends/resource/legal-notices/OpenDS.LICENSE
  ! or https://OpenDS.dev.java.net/OpenDS.LICENSE.
  ! 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
  ! trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
  !
  !
  !      Copyright 2013 ForgeRock AS
  ! -->
<adm:managed-object name="file-based-http-access-log-publisher"
  plural-name="file-based-http-access-log-publishers"
  package="org.opends.server.admin.std" extends="http-access-log-publisher"
  xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap">
  <adm:synopsis>
    <adm:user-friendly-plural-name />
    publish HTTP access messages to the file system.
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-file-based-http-access-log-publisher</ldap:name>
      <ldap:superior>ds-cfg-http-access-log-publisher</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class" advanced="true">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.loggers.TextHTTPAccessLogPublisher
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
  <adm:property name="asynchronous" mandatory="true" advanced="true">
    <adm:synopsis>
      Indicates whether the
      <adm:user-friendly-name />
      will publish records asynchronously.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>true</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-asynchronous</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="queue-size" advanced="true">
    <adm:synopsis>
      The maximum number of log records that can be stored in the
      asynchronous queue.
    </adm:synopsis>
    <adm:description>
      Setting the queue size to zero activates parallel log writer
      implementation which has no queue size limit and as such the
      parallel log writer should only be used on a very well tuned
      server configuration to avoid potential out of memory errors.
    </adm:description>
    <adm:requires-admin-action>
      <adm:other>
        <adm:synopsis>
          The <adm:user-friendly-name /> must be restarted if this property
          is changed and the asynchronous property is set to true.
        </adm:synopsis>
      </adm:other>
    </adm:requires-admin-action>
        <adm:default-behavior>
      <adm:defined>
        <adm:value>5000</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:integer lower-limit="0" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-queue-size</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="log-file" mandatory="true">
    <adm:synopsis>
      The file name to use for the log files generated by the
      <adm:user-friendly-name />.
      The path to the file is relative to the server root.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:component-restart />
    </adm:requires-admin-action>
   <adm:syntax>
     <adm:string>
       <adm:pattern>
        <adm:regex>.*</adm:regex>
        <adm:usage>FILE</adm:usage>
          <adm:synopsis>
            A path to an existing file that is readable by the server.
          </adm:synopsis>
        </adm:pattern>
      </adm:string>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-log-file</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="log-file-permissions" mandatory="true">
    <adm:synopsis>
      The UNIX permissions of the log files created by this
      <adm:user-friendly-name />.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>640</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:string>
        <adm:pattern>
          <adm:regex>^([0-7][0-7][0-7])$</adm:regex>
          <adm:usage>MODE</adm:usage>
          <adm:synopsis>
            A valid UNIX mode string. The mode string must contain
            three digits between zero and seven.
          </adm:synopsis>
        </adm:pattern>
      </adm:string>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-log-file-permissions</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="time-interval" advanced="true">
    <adm:synopsis>
      Specifies the interval at which to check whether the log files
      need to be rotated.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>5s</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:duration base-unit="ms" lower-limit="1" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-time-interval</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="buffer-size" advanced="true">
    <adm:synopsis>Specifies the log file buffer size.</adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>64kb</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:size lower-limit="1" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-buffer-size</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="auto-flush" advanced="true">
    <adm:synopsis>
      Specifies whether to flush the writer after every log record.
    </adm:synopsis>
    <adm:description>
      If the asynchronous writes option is used, the writer is
      flushed after all the log records in the queue are written.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>true</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-auto-flush</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="append">
    <adm:synopsis>
      Specifies whether to append to existing log files.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>true</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-append</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="rotation-policy" multi-valued="true">
    <adm:synopsis>
      The rotation policy to use for the
      <adm:user-friendly-name />
      .
    </adm:synopsis>
    <adm:description>
      When multiple policies are used, rotation will occur if any
      policy's conditions are met.
    </adm:description>
    <adm:default-behavior>
      <adm:alias>
        <adm:synopsis>
          No rotation policy is used and log rotation will not occur.
        </adm:synopsis>
      </adm:alias>
    </adm:default-behavior>
    <adm:syntax>
      <adm:aggregation parent-path="/"
        relation-name="log-rotation-policy" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-rotation-policy</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="retention-policy" multi-valued="true">
    <adm:synopsis>
      The retention policy to use for the
      <adm:user-friendly-name />
      .
    </adm:synopsis>
    <adm:description>
      When multiple policies are used, log files are cleaned when
      any of the policy's conditions are met.
    </adm:description>
    <adm:default-behavior>
      <adm:alias>
        <adm:synopsis>
          No retention policy is used and log files are never cleaned.
        </adm:synopsis>
      </adm:alias>
    </adm:default-behavior>
    <adm:syntax>
      <adm:aggregation parent-path="/"
        relation-name="log-retention-policy" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-retention-policy</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="log-record-time-format">
    <adm:synopsis>
      Specifies the format string that is used to generate log record
      timestamps.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>dd/MMM/yyyy:HH:mm:ss Z</adm:value>
      </adm:defined>
    </adm:default-behavior>
   <adm:syntax>
     <adm:string>
       <adm:pattern>
        <adm:regex>.*</adm:regex>
        <adm:usage>STRING</adm:usage>
          <adm:synopsis>
            Any valid format string that can be used with the
            java.text.SimpleDateFormat class.
          </adm:synopsis>
        </adm:pattern>
      </adm:string>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-log-record-time-format</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opends/src/admin/defn/org/opends/server/admin/std/HTTPAccessLogPublisherConfiguration.xml
New file
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
  ! 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
  ! trunk/opends/resource/legal-notices/OpenDS.LICENSE
  ! or https://OpenDS.dev.java.net/OpenDS.LICENSE.
  ! 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
  ! trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
  !
  !
  !      Copyright 2013 ForgeRock AS
  ! -->
<adm:managed-object name="http-access-log-publisher"
  plural-name="http-access-log-publishers"
  package="org.opends.server.admin.std" extends="log-publisher"
  xmlns:adm="http://www.opends.org/admin"
  xmlns:ldap="http://www.opends.org/admin-ldap"
  xmlns:cli="http://www.opends.org/admin-cli">
  <adm:synopsis>
    <adm:user-friendly-plural-name />
    are responsible for distributing HTTP access log messages from the HTTP
    access logger to a destination.
  </adm:synopsis>
  <adm:description>
    HTTP access log messages provide information about the types of HTTP
    requests processed by the server.
  </adm:description>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-http-access-log-publisher</ldap:name>
      <ldap:superior>ds-cfg-log-publisher</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:profile name="cli">
    <cli:managed-object custom="true" />
  </adm:profile>
  <adm:property-override name="java-class">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>org.opends.server.api.HTTPAccessLogPublisher</adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opends/src/admin/messages/FileBasedHTTPAccessLogPublisherCfgDefn.properties
New file
@@ -0,0 +1,26 @@
user-friendly-name=File Based HTTP Access Log Publisher
user-friendly-plural-name=File Based HTTP Access Log Publishers
synopsis=File Based HTTP Access Log Publishers publish HTTP access messages to the file system.
property.append.synopsis=Specifies whether to append to existing log files.
property.asynchronous.synopsis=Indicates whether the File Based HTTP Access Log Publisher will publish records asynchronously.
property.auto-flush.synopsis=Specifies whether to flush the writer after every log record.
property.auto-flush.description=If the asynchronous writes option is used, the writer is flushed after all the log records in the queue are written.
property.buffer-size.synopsis=Specifies the log file buffer size.
property.enabled.synopsis=Indicates whether the File Based HTTP Access Log Publisher is enabled for use.
property.java-class.synopsis=The fully-qualified name of the Java class that provides the File Based HTTP Access Log Publisher implementation.
property.log-file.synopsis=The file name to use for the log files generated by the File Based HTTP Access Log Publisher. The path to the file is relative to the server root.
property.log-file.syntax.string.pattern.synopsis=A path to an existing file that is readable by the server.
property.log-file-permissions.synopsis=The UNIX permissions of the log files created by this File Based HTTP Access Log Publisher.
property.log-file-permissions.syntax.string.pattern.synopsis=A valid UNIX mode string. The mode string must contain three digits between zero and seven.
property.log-record-time-format.synopsis=Specifies the format string that is used to generate log record timestamps.
property.log-record-time-format.syntax.string.pattern.synopsis=Any valid format string that can be used with the java.text.SimpleDateFormat class.
property.queue-size.synopsis=The maximum number of log records that can be stored in the asynchronous queue.
property.queue-size.description=Setting the queue size to zero activates parallel log writer implementation which has no queue size limit and as such the parallel log writer should only be used on a very well tuned server configuration to avoid potential out of memory errors.
property.queue-size.requires-admin-action.synopsis=The File Based HTTP Access Log Publisher must be restarted if this property is changed and the asynchronous property is set to true.
property.retention-policy.synopsis=The retention policy to use for the File Based HTTP Access Log Publisher .
property.retention-policy.description=When multiple policies are used, log files are cleaned when any of the policy's conditions are met.
property.retention-policy.default-behavior.alias.synopsis=No retention policy is used and log files are never cleaned.
property.rotation-policy.synopsis=The rotation policy to use for the File Based HTTP Access Log Publisher .
property.rotation-policy.description=When multiple policies are used, rotation will occur if any policy's conditions are met.
property.rotation-policy.default-behavior.alias.synopsis=No rotation policy is used and log rotation will not occur.
property.time-interval.synopsis=Specifies the interval at which to check whether the log files need to be rotated.
opends/src/admin/messages/HTTPAccessLogPublisherCfgDefn.properties
New file
@@ -0,0 +1,6 @@
user-friendly-name=HTTP Access Log Publisher
user-friendly-plural-name=HTTP Access Log Publishers
synopsis=HTTP Access Log Publishers are responsible for distributing HTTP access log messages from the HTTP access logger to a destination.
description=HTTP access log messages provide information about the types of HTTP requests processed by the server.
property.enabled.synopsis=Indicates whether the HTTP Access Log Publisher is enabled for use.
property.java-class.synopsis=The fully-qualified name of the Java class that provides the HTTP Access Log Publisher implementation.
opends/src/messages/messages/config.properties
@@ -165,9 +165,6 @@
SEVERE_WARN_CONFIG_LOGGER_NO_ACTIVE_ERROR_LOGGERS_45=There are no active \
 error loggers defined in the Directory Server configuration.  No error \
 logging will be performed
MILD_WARN_CONFIG_LOGGER_NO_ACTIVE_DEBUG_LOGGERS_46=There are no active debug \
 loggers defined in the Directory Server configuration.  No debug logging will \
 be performed
SEVERE_ERR_CONFIG_LOGGER_ENTRY_UNACCEPTABLE_47=Configuration entry %s does \
 not contain a valid logger configuration:  %s.  It will be ignored
INFO_CONFIG_UNKNOWN_UNACCEPTABLE_REASON_48=Unknown unacceptable reason
@@ -2178,3 +2175,9 @@
SEVERE_ERR_CONFIG_LOGGING_INVALID_TARGET_DN_PATTERN_730=Thee access log filtering \
 criteria defined in "%s" could not be parsed because it contains an invalid \
 target DN pattern "%s"
SEVERE_WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS_731=There are no active \
 HTTP access loggers defined in the Directory Server configuration.  No HTTP \
 access logging will be performed
SEVERE_ERR_CONFIG_LOGGER_INVALID_HTTP_ACCESS_LOGGER_CLASS_732=Class %s specified \
 in attribute ds-cfg-java-class of configuration entry %s cannot be \
 instantiated as a Directory Server HTTP access logger:  %s
opends/src/server/org/opends/server/api/HTTPAccessLogPublisher.java
New file
@@ -0,0 +1,101 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Copyright 2013 ForgeRock AS.
 */
package org.opends.server.api;
import java.util.List;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.HTTPAccessLogPublisherCfg;
import org.opends.server.loggers.HTTPRequestInfo;
/**
 * This class defines the set of methods and structures that must be implemented
 * for a Directory Server HTTP access log publisher.
 *
 * @param <T>
 *          The type of HTTP access log publisher configuration handled by this
 *          log publisher implementation.
 */
@org.opends.server.types.PublicAPI(
    stability = org.opends.server.types.StabilityLevel.VOLATILE,
    mayInstantiate = false,
    mayExtend = true,
    mayInvoke = false)
public abstract class HTTPAccessLogPublisher
    <T extends HTTPAccessLogPublisherCfg> implements LogPublisher<T>
{
  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationAcceptable(T configuration,
      List<Message> unacceptableReasons)
  {
    // This default implementation does not perform any special
    // validation. It should be overridden by HTTP access log publisher
    // implementations that wish to perform more detailed validation.
    return true;
  }
  /**
   * Logs the request info according to the common logfile format. The common
   * logfile format is as follows:
   *
   * <pre>
   * remotehost rfc931 authuser [date] "request" status bytes "useragent"
   * </pre>
   * <dl>
   * <dt>remotehost</dt>
   * <dd>Remote hostname (or IP number if DNS hostname is not available, or if
   * DNSLookup is Off.</dd>
   * <dt>rfc931</dt>
   * <dd>The remote logname of the user.</dd>
   * <dt>authuser</dt>
   * <dd>The username as which the user has authenticated himself.</dd>
   * <dt>[date]</dt>
   * <dd>Date and time of the request.</dd>
   * <dt>"request"</dt>
   * <dd>The request line exactly as it came from the client.</dd>
   * <dt>status</dt>
   * <dd>The HTTP status code returned to the client.</dd>
   * <dt>bytes</dt>
   * <dd>The content-length of the document transferred.</dd>
   * <dt>"useragent"</dt>
   * <dd>The user agent that issued the request.</dd>
   * </dl>
   * <p>
   * <b>NOTE:</b> The bytes field is not currently supported.
   * </p>
   *
   * @param requestInfo
   *          The request info to log
   */
  public void logRequestInfo(HTTPRequestInfo requestInfo)
  {
    // Do nothing
  }
}
opends/src/server/org/opends/server/core/LoggerConfigManager.java
@@ -37,11 +37,17 @@
import org.opends.server.admin.server.ConfigurationAddListener;
import org.opends.server.admin.server.ConfigurationDeleteListener;
import org.opends.server.admin.server.ServerManagementContext;
import org.opends.server.admin.std.server.*;
import org.opends.server.admin.std.server.AccessLogPublisherCfg;
import org.opends.server.admin.std.server.DebugLogPublisherCfg;
import org.opends.server.admin.std.server.ErrorLogPublisherCfg;
import org.opends.server.admin.std.server.HTTPAccessLogPublisherCfg;
import org.opends.server.admin.std.server.LogPublisherCfg;
import org.opends.server.admin.std.server.RootCfg;
import org.opends.server.config.ConfigException;
import org.opends.server.loggers.AbstractLogger;
import org.opends.server.loggers.AccessLogger;
import org.opends.server.loggers.ErrorLogger;
import org.opends.server.loggers.HTTPAccessLogger;
import org.opends.server.loggers.debug.DebugLogger;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.InitializationException;
@@ -87,6 +93,9 @@
    List<AccessLogPublisherCfg> accessPublisherCfgs =
        new ArrayList<AccessLogPublisherCfg>();
    List<HTTPAccessLogPublisherCfg> httpAccessPublisherCfgs =
        new ArrayList<HTTPAccessLogPublisherCfg>();
    List<ErrorLogPublisherCfg> errorPublisherCfgs =
        new ArrayList<ErrorLogPublisherCfg>();
@@ -102,6 +111,10 @@
      {
        accessPublisherCfgs.add((AccessLogPublisherCfg)config);
      }
      else if (config instanceof HTTPAccessLogPublisherCfg)
      {
        httpAccessPublisherCfgs.add((HTTPAccessLogPublisherCfg) config);
      }
      else if(config instanceof ErrorLogPublisherCfg)
      {
        errorPublisherCfgs.add((ErrorLogPublisherCfg)config);
@@ -116,10 +129,16 @@
    // See if there are active loggers in all categories.  If not, then log a
    // message.
    // Do not output warn message for debug loggers because it is valid to fully
    // disable all debug loggers.
    if (accessPublisherCfgs.isEmpty())
    {
      logError(WARN_CONFIG_LOGGER_NO_ACTIVE_ACCESS_LOGGERS.get());
    }
    if (httpAccessPublisherCfgs.isEmpty())
    {
      logError(WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS.get());
    }
    if (errorPublisherCfgs.isEmpty())
    {
      logError(WARN_CONFIG_LOGGER_NO_ACTIVE_ERROR_LOGGERS.get());
@@ -127,6 +146,7 @@
    DebugLogger.getInstance().initializeLogger(debugPublisherCfgs);
    AccessLogger.getInstance().initializeLogger(accessPublisherCfgs);
    HTTPAccessLogger.getInstance().initializeLogger(httpAccessPublisherCfgs);
    ErrorLogger.getInstance().initializeLogger(errorPublisherCfgs);
  }
@@ -154,6 +174,10 @@
    {
      return AccessLogger.getInstance();
    }
    else if (config instanceof HTTPAccessLogPublisherCfg)
    {
      return HTTPAccessLogger.getInstance();
    }
    else if (config instanceof ErrorLogPublisherCfg)
    {
      return ErrorLogger.getInstance();
opends/src/server/org/opends/server/loggers/HTTPAccessLogger.java
New file
@@ -0,0 +1,148 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Copyright 2013 ForgeRock AS
 */
package org.opends.server.loggers;
import static org.opends.messages.ConfigMessages.*;
import java.util.Collection;
import org.opends.server.admin.ClassPropertyDefinition;
import org.opends.server.admin.std.meta.HTTPAccessLogPublisherCfgDefn;
import org.opends.server.admin.std.server.HTTPAccessLogPublisherCfg;
import org.opends.server.api.HTTPAccessLogPublisher;
/**
 * This class defines the wrapper that will invoke all registered HTTP access
 * loggers for each type of request received or response sent.
 */
public class HTTPAccessLogger extends AbstractLogger
<HTTPAccessLogPublisher<HTTPAccessLogPublisherCfg>, HTTPAccessLogPublisherCfg>
{
  private static LoggerStorage
  <HTTPAccessLogPublisher<HTTPAccessLogPublisherCfg>, HTTPAccessLogPublisherCfg>
  loggerStorage = new LoggerStorage
  <HTTPAccessLogPublisher<HTTPAccessLogPublisherCfg>, HTTPAccessLogPublisherCfg>
  ();
  /** The singleton instance of this class for configuration purposes. */
  private static final HTTPAccessLogger instance = new HTTPAccessLogger();
  /**
   * The constructor for this class.
   */
  public HTTPAccessLogger()
  {
    super((Class) HTTPAccessLogPublisher.class,
        ERR_CONFIG_LOGGER_INVALID_HTTP_ACCESS_LOGGER_CLASS);
  }
  /** {@inheritDoc} */
  @Override
  protected ClassPropertyDefinition getJavaClassPropertyDefinition()
  {
    return HTTPAccessLogPublisherCfgDefn.getInstance()
        .getJavaClassPropertyDefinition();
  }
  /** {@inheritDoc} */
  @Override
  protected LoggerStorage<HTTPAccessLogPublisher<HTTPAccessLogPublisherCfg>,
      HTTPAccessLogPublisherCfg> getStorage()
  {
    return loggerStorage;
  }
  /**
   * Retrieve the singleton instance of this class.
   *
   * @return The singleton instance of this logger.
   */
  public static HTTPAccessLogger getInstance()
  {
    return instance;
  }
  /**
   * Add an HTTP access log publisher to the HTTP access logger.
   *
   * @param publisher
   *          The HTTP access log publisher to add.
   */
  public synchronized static void addHTTPAccessLogPublisher(
      HTTPAccessLogPublisher publisher)
  {
    loggerStorage.addLogPublisher(publisher);
  }
  /**
   * Remove an HTTP access log publisher from the HTTP access logger.
   *
   * @param publisher
   *          The HTTP access log publisher to remove.
   * @return The publisher that was removed or null if it was not found.
   */
  public synchronized static boolean removeHTTPAccessLogPublisher(
      HTTPAccessLogPublisher<HTTPAccessLogPublisherCfg> publisher)
  {
    return loggerStorage.removeLogPublisher(publisher);
  }
  /**
   * Removes all existing HTTP access log publishers from the logger.
   */
  public synchronized static void removeAllHTTPAccessLogPublishers()
  {
    loggerStorage.removeAllLogPublishers();
  }
  /**
   * Returns all the registered HTTP access log publishers.
   *
   * @return a Collection of {@link HTTPAccessLogPublisher} objects
   */
  private static Collection<HTTPAccessLogPublisher<HTTPAccessLogPublisherCfg>>
      getHTTPAccessLogPublishers()
  {
    return loggerStorage.getLogPublishers();
  }
  /**
   * Logs the given HTTPRequestInfo.
   *
   * @param requestInfo
   *          the HTTP request info to log
   */
  public static void logRequestInfo(HTTPRequestInfo requestInfo)
  {
    for (HTTPAccessLogPublisher<?> publisher : loggerStorage.getLogPublishers())
    {
      publisher.logRequestInfo(requestInfo);
    }
  }
}
opends/src/server/org/opends/server/loggers/HTTPRequestInfo.java
New file
@@ -0,0 +1,177 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Copyright 2013 ForgeRock AS
 */
package org.opends.server.loggers;
import javax.servlet.http.HttpServletRequest;
/**
 * Contains the information required for logging the HTTP request.
 */
public class HTTPRequestInfo
{
  /** The client's host. */
  private final String remoteHost;
  /** The client's address. */
  private final String remoteAddress;
  /** The protocol used for this request. */
  private final String protocol;
  /** The HTTP method/verb used for this request. */
  private final String method;
  /** The query issued by the client. */
  private final String query;
  /** The user agent used by the client. */
  private final String userAgent;
  /** The username that was used to authenticate. */
  private String authUser;
  /** The HTTP status code returned to the client. */
  private volatile Integer statusCode;
  /**
   * Constructor for this class.
   *
   * @param request
   *          The {@link HttpServletRequest} for which to log the information
   */
  public HTTPRequestInfo(HttpServletRequest request)
  {
    this.remoteHost = request.getRemoteHost();
    this.remoteAddress = request.getRemoteAddr();
    this.method = request.getMethod();
    this.query = request.getRequestURI() + "/" + request.getQueryString();
    this.protocol = request.getProtocol();
    this.userAgent = request.getHeader("User-Agent");
  }
  /**
   * Returns the client's host.
   *
   * @return the remoteHost
   */
  public String getRemoteHost()
  {
    return remoteHost;
  }
  /**
   * Returns the client's address.
   *
   * @return the remoteAddress
   */
  public String getRemoteAddress()
  {
    return remoteAddress;
  }
  /**
   * Returns the protocol used for this request.
   *
   * @return the protocol
   */
  public String getProtocol()
  {
    return protocol;
  }
  /**
   * Returns the HTTP method/verb used for this request.
   *
   * @return the method
   */
  public String getMethod()
  {
    return method;
  }
  /**
   * Returns the query issued by the client.
   *
   * @return the query
   */
  public String getQuery()
  {
    return query;
  }
  /**
   * Returns the user agent used by the client.
   *
   * @return the userAgent
   */
  public String getUserAgent()
  {
    return userAgent;
  }
  /**
   * Returns the username that was used to authenticate.
   *
   * @return the authUser
   */
  public String getAuthUser()
  {
    return authUser;
  }
  /**
   * Sets the username that was used to authenticate.
   *
   * @param authUser
   *          the authUser to set
   */
  public void setAuthUser(String authUser)
  {
    this.authUser = authUser;
  }
  /**
   * Returns the HTTP status code returned to the client.
   *
   * @return the statusCode
   */
  public int getStatusCode()
  {
    return statusCode != null ? statusCode : 200;
  }
  /**
   * Logs the current request info in the HTTP access log.
   *
   * @param statusCode
   *          the HTTP status code that was returned to the client.
   */
  public void log(int statusCode)
  {
    if (this.statusCode == null)
    { // this request was not logged before
      this.statusCode = statusCode;
      HTTPAccessLogger.logRequestInfo(this);
    }
  }
}
opends/src/server/org/opends/server/loggers/TextHTTPAccessLogPublisher.java
New file
@@ -0,0 +1,409 @@
/*
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * 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
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
 *
 *
 *      Copyright 2013 ForgeRock AS
 */
package org.opends.server.loggers;
import static org.opends.messages.ConfigMessages.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import org.opends.messages.Message;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.FileBasedHTTPAccessLogPublisherCfg;
import org.opends.server.api.HTTPAccessLogPublisher;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.FilePermission;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.TimeThread;
/**
 * This class provides the implementation of the HTTP access logger used by the
 * directory server.
 */
public final class TextHTTPAccessLogPublisher extends
    HTTPAccessLogPublisher<FileBasedHTTPAccessLogPublisherCfg>
    implements ConfigurationChangeListener<FileBasedHTTPAccessLogPublisherCfg>
{
  /**
   * Returns an instance of the text HTTP access log publisher that will print
   * all messages to the provided writer. This is used to print the messages to
   * the console when the server starts up.
   *
   * @param writer
   *          The text writer where the message will be written to.
   * @return The instance of the text error log publisher that will print all
   *         messages to standard out.
   */
  public static TextHTTPAccessLogPublisher getStartupTextHTTPAccessPublisher(
      final TextWriter writer)
  {
    final TextHTTPAccessLogPublisher startupPublisher =
      new TextHTTPAccessLogPublisher();
    startupPublisher.writer = writer;
    return startupPublisher;
  }
  private TextWriter writer = null;
  private FileBasedHTTPAccessLogPublisherCfg cfg = null;
  private String timeStampFormat = "dd/MMM/yyyy:HH:mm:ss Z";
  private DateFormat dateFormatter = new SimpleDateFormat(timeStampFormat);
  /** {@inheritDoc} */
  @Override
  public ConfigChangeResult applyConfigurationChange(
      final FileBasedHTTPAccessLogPublisherCfg config)
  {
    // Default result code.
    ResultCode resultCode = ResultCode.SUCCESS;
    boolean adminActionRequired = false;
    final ArrayList<Message> messages = new ArrayList<Message>();
    final File logFile = getFileForPath(config.getLogFile());
    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
    try
    {
      final FilePermission perm = FilePermission.decodeUNIXMode(config
          .getLogFilePermissions());
      final boolean writerAutoFlush = config.isAutoFlush()
          && !config.isAsynchronous();
      TextWriter currentWriter;
      // Determine the writer we are using. If we were writing
      // asynchronously, we need to modify the underlying writer.
      if (writer instanceof AsyncronousTextWriter)
      {
        currentWriter = ((AsyncronousTextWriter) writer).getWrappedWriter();
      }
      else if (writer instanceof ParallelTextWriter)
      {
        currentWriter = ((ParallelTextWriter) writer).getWrappedWriter();
      }
      else
      {
        currentWriter = writer;
      }
      if (currentWriter instanceof MultifileTextWriter)
      {
        final MultifileTextWriter mfWriter =
          (MultifileTextWriter) currentWriter;
        mfWriter.setNamingPolicy(fnPolicy);
        mfWriter.setFilePermissions(perm);
        mfWriter.setAppend(config.isAppend());
        mfWriter.setAutoFlush(writerAutoFlush);
        mfWriter.setBufferSize((int) config.getBufferSize());
        mfWriter.setInterval(config.getTimeInterval());
        mfWriter.removeAllRetentionPolicies();
        mfWriter.removeAllRotationPolicies();
        for (final DN dn : config.getRotationPolicyDNs())
        {
          mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
        }
        for (final DN dn : config.getRetentionPolicyDNs())
        {
          mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
        }
        if (writer instanceof AsyncronousTextWriter && !config.isAsynchronous())
        {
          // The asynchronous setting is being turned off.
          final AsyncronousTextWriter asyncWriter =
            ((AsyncronousTextWriter) writer);
          writer = mfWriter;
          asyncWriter.shutdown(false);
        }
        if (writer instanceof ParallelTextWriter && !config.isAsynchronous())
        {
          // The asynchronous setting is being turned off.
          final ParallelTextWriter asyncWriter = ((ParallelTextWriter) writer);
          writer = mfWriter;
          asyncWriter.shutdown(false);
        }
        if (!(writer instanceof AsyncronousTextWriter)
            && config.isAsynchronous())
        {
          // The asynchronous setting is being turned on.
          final AsyncronousTextWriter asyncWriter = new AsyncronousTextWriter(
              "Asyncronous Text Writer for " + config.dn().toNormalizedString(),
              config.getQueueSize(), config.isAutoFlush(), mfWriter);
          writer = asyncWriter;
        }
        if (!(writer instanceof ParallelTextWriter) && config.isAsynchronous())
        {
          // The asynchronous setting is being turned on.
          final ParallelTextWriter asyncWriter = new ParallelTextWriter(
              "Parallel Text Writer for " + config.dn().toNormalizedString(),
              config.isAutoFlush(), mfWriter);
          writer = asyncWriter;
        }
        if ((cfg.isAsynchronous() && config.isAsynchronous())
            && (cfg.getQueueSize() != config.getQueueSize()))
        {
          adminActionRequired = true;
        }
        if (!config.getLogRecordTimeFormat().equals(timeStampFormat))
        {
          TimeThread.removeUserDefinedFormatter(timeStampFormat);
          setTimeStampFormat(config.getLogRecordTimeFormat());
        }
        cfg = config;
      }
    }
    catch (final Exception e)
    {
      final Message message = ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
          config.dn().toString(), stackTraceToSingleLineString(e));
      resultCode = DirectoryServer.getServerErrorResultCode();
      messages.add(message);
    }
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
  /** {@inheritDoc} */
  @Override
  public void initializeLogPublisher(
      final FileBasedHTTPAccessLogPublisherCfg cfg)
      throws ConfigException, InitializationException
  {
    final File logFile = getFileForPath(cfg.getLogFile());
    final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
    try
    {
      final FilePermission perm = FilePermission.decodeUNIXMode(cfg
          .getLogFilePermissions());
      final LogPublisherErrorHandler errorHandler =
        new LogPublisherErrorHandler(cfg.dn());
      final boolean writerAutoFlush = cfg.isAutoFlush()
          && !cfg.isAsynchronous();
      final MultifileTextWriter theWriter = new MultifileTextWriter(
          "Multifile Text Writer for " + cfg.dn().toNormalizedString(),
          cfg.getTimeInterval(), fnPolicy, perm, errorHandler, "UTF-8",
          writerAutoFlush, cfg.isAppend(), (int) cfg.getBufferSize());
      // Validate retention and rotation policies.
      for (final DN dn : cfg.getRotationPolicyDNs())
      {
        theWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
      }
      for (final DN dn : cfg.getRetentionPolicyDNs())
      {
        theWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
      }
      if (cfg.isAsynchronous())
      {
        if (cfg.getQueueSize() > 0)
        {
          this.writer = new AsyncronousTextWriter(
              "Asyncronous Text Writer for " + cfg.dn().toNormalizedString(),
              cfg.getQueueSize(), cfg.isAutoFlush(), theWriter);
        }
        else
        {
          this.writer = new ParallelTextWriter("Parallel Text Writer for "
              + cfg.dn().toNormalizedString(), cfg.isAutoFlush(), theWriter);
        }
      }
      else
      {
        this.writer = theWriter;
      }
    }
    catch (final DirectoryException e)
    {
      final Message message = ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg
          .dn().toString(), String.valueOf(e));
      throw new InitializationException(message, e);
    }
    catch (final IOException e)
    {
      final Message message = ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(
          logFile.toString(), cfg.dn().toString(), String.valueOf(e));
      throw new InitializationException(message, e);
    }
    this.cfg = cfg;
    setTimeStampFormat(cfg.getLogRecordTimeFormat());
    cfg.addFileBasedHTTPAccessChangeListener(this);
  }
  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationAcceptable(
      final FileBasedHTTPAccessLogPublisherCfg configuration,
      final List<Message> unacceptableReasons)
  {
    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
  }
  /** {@inheritDoc} */
  @Override
  public boolean isConfigurationChangeAcceptable(
      final FileBasedHTTPAccessLogPublisherCfg config,
      final List<Message> unacceptableReasons)
  {
    // Validate the time-stamp formatter.
    final String formatString = config.getLogRecordTimeFormat();
    try
    {
       new SimpleDateFormat(formatString);
    }
    catch (final Exception e)
    {
      final Message message = ERR_CONFIG_LOGGING_INVALID_TIME_FORMAT.get(String
          .valueOf(formatString));
      unacceptableReasons.add(message);
      return false;
    }
    // Make sure the permission is valid.
    try
    {
      final FilePermission filePerm = FilePermission.decodeUNIXMode(config
          .getLogFilePermissions());
      if (!filePerm.isOwnerWritable())
      {
        final Message message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config
            .getLogFilePermissions());
        unacceptableReasons.add(message);
        return false;
      }
    }
    catch (final DirectoryException e)
    {
      final Message message = ERR_CONFIG_LOGGING_MODE_INVALID.get(
          config.getLogFilePermissions(), String.valueOf(e));
      unacceptableReasons.add(message);
      return false;
    }
    return true;
  }
  /** {@inheritDoc} */
  @Override
  public final void close()
  {
    writer.shutdown();
    TimeThread.removeUserDefinedFormatter(timeStampFormat);
    if (cfg != null)
    {
      cfg.removeFileBasedHTTPAccessChangeListener(this);
    }
  }
  private void setTimeStampFormat(String timeStampFormat)
  {
    this.timeStampFormat = timeStampFormat;
    this.dateFormatter = new SimpleDateFormat(timeStampFormat);
  }
  /** {@inheritDoc} */
  @Override
  public final DN getDN()
  {
    return cfg != null ? cfg.dn() : null;
  }
  /** {@inheritDoc} */
  @Override
  public void logRequestInfo(HTTPRequestInfo ri)
  {
    final StringBuilder sb = new StringBuilder(100);
    // remotehost
    if (ri.getRemoteHost() != null)
    {
      sb.append(ri.getRemoteHost());
    }
    else
    {
      sb.append(ri.getRemoteAddress());
    }
    // rfc931 - not supported
    // authuser
    sb.append(" ");
    if (ri.getAuthUser() != null)
    {
      sb.append(ri.getAuthUser());
    }
    // [dateAndTime]
    sb.append(" [").append(TimeThread.getUserDefinedTime(timeStampFormat))
        .append("]");
    // "request"
    sb.append(" \"").append(ri.getMethod());
    sb.append(" ").append(ri.getQuery());
    sb.append(" ").append(ri.getProtocol()).append("\"");
    // HTTP response status code
    sb.append(" ").append(ri.getStatusCode());
    // bytes - not supported
    // "user agent"
    sb.append(" \"").append(ri.getUserAgent()).append("\"");
    writer.writeRecord(sb.toString());
  }
}
opends/src/server/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -44,11 +44,11 @@
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.opendj.ldap.Connection;
@@ -66,6 +66,7 @@
import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
import org.opends.server.loggers.HTTPRequestInfo;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.schema.SchemaConstants;
import org.opends.server.types.AddressMask;
@@ -85,8 +86,8 @@
  private final class HTTPRequestContext
  {
    private AsyncContext asyncContext;
    private ServletRequest request;
    private ServletResponse response;
    private HttpServletRequest request;
    private HttpServletResponse response;
    private FilterChain chain;
    private HTTPClientConnection clientConnection;
@@ -94,8 +95,12 @@
    /** Whether to pretty print the resulting JSON. */
    private boolean prettyPrint;
    /** Can be used for the bind request. */
    /** Used for the bind request when credentials are specified. */
    private String userName;
    /** Used for the bind request when credentials are specified. */
    private String password;
    /** Request information for logging. */
    private HTTPRequestInfo requestInfo;
  }
  /**
@@ -176,6 +181,8 @@
    @Override
    public void handleResult(BindResult result)
    {
      ctx.requestInfo.setAuthUser(ctx.userName);
      final AuthenticationInfo authInfo =
          new AuthenticationInfo(to(resultEntry), to(resultEntry.getName()),
              ByteString.valueOf(ctx.password), false);
@@ -231,23 +238,44 @@
  /** {@inheritDoc} */
  @Override
  public void doFilter(ServletRequest request, ServletResponse response,
  public void doFilter(ServletRequest req, ServletResponse resp,
      FilterChain chain)
  {
    final boolean prettyPrint =
    final HttpServletRequest request = (HttpServletRequest) req;
    final HttpServletResponse response = (HttpServletResponse) resp;
    final HTTPRequestContext ctx = new HTTPRequestContext();
    ctx.request = request;
    ctx.response = new HttpServletResponseWrapper(response)
    {
      /** {@inheritDoc} */
      @Override
      public void setStatus(int sc)
      {
        ctx.requestInfo.log(sc);
        super.setStatus(sc);
      }
      /** {@inheritDoc} */
      @Override
      public void setStatus(int sc, String sm)
      {
        ctx.requestInfo.log(sc);
        super.setStatus(sc, sm);
      }
    };
    ctx.chain = chain;
    ctx.prettyPrint =
        Boolean.parseBoolean(request.getParameter("_prettyPrint"));
    ctx.requestInfo = new HTTPRequestInfo(ctx.request);
    final HTTPClientConnection clientConnection =
        new HTTPClientConnection(this.connectionHandler, request);
    this.connectionHandler.addClientConnection(clientConnection);
    final HTTPRequestContext ctx = new HTTPRequestContext();
    ctx.request = request;
    ctx.response = response;
    ctx.chain = chain;
    ctx.clientConnection = clientConnection;
    ctx.prettyPrint = prettyPrint;
    try
    {
@@ -259,17 +287,18 @@
      // checked.
      logConnect(clientConnection);
      ctx.connection = new SdkConnectionAdapter(clientConnection);
      ctx.connection =
          new SdkConnectionAdapter(clientConnection, ctx.requestInfo);
      final String[] userPassword = extractUsernamePassword(request);
      if (userPassword != null && userPassword.length == 2)
      {
        final String userName = userPassword[0];
        ctx.userName = userPassword[0];
        ctx.password = userPassword[1];
        ctx.asyncContext = getAsyncContext(request);
        ctx.connection.searchSingleEntryAsync(buildSearchRequest(userName),
        ctx.connection.searchSingleEntryAsync(buildSearchRequest(ctx.userName),
            new DoBindResultHandler(ctx));
      }
      else if (this.connectionHandler.acceptUnauthenticatedRequests())
@@ -308,13 +337,13 @@
  private void sendAuthenticationFailure(HTTPRequestContext ctx)
  {
    final int statusCode = HttpServletResponse.SC_UNAUTHORIZED;
    try
    {
      // The user could not be authenticated. Send an HTTP Basic authentication
      // challenge if HTTP Basic authentication is enabled.
      ResourceException unauthorizedException =
          ResourceException.getException(HttpServletResponse.SC_UNAUTHORIZED,
              "Invalid Credentials");
          ResourceException.getException(statusCode, "Invalid Credentials");
      sendErrorReponse(ctx.response, ctx.prettyPrint, unauthorizedException);
      ctx.clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS,
@@ -322,6 +351,8 @@
    }
    finally
    {
      ctx.requestInfo.log(statusCode);
      if (ctx.asyncContext != null)
      {
        ctx.asyncContext.complete();
@@ -331,6 +362,7 @@
  private void onFailure(Exception e, HTTPRequestContext ctx)
  {
    ResourceException ex = Rest2LDAP.asResourceException(e);
    try
    {
      if (debugEnabled())
@@ -338,8 +370,7 @@
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      sendErrorReponse(ctx.response, ctx.prettyPrint, Rest2LDAP
          .asResourceException(e));
      sendErrorReponse(ctx.response, ctx.prettyPrint, ex);
      Message message =
          INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(ctx.clientConnection
@@ -352,6 +383,8 @@
    }
    finally
    {
      ctx.requestInfo.log(ex.getCode());
      if (ctx.asyncContext != null)
      {
        ctx.asyncContext.complete();
@@ -359,7 +392,7 @@
    }
  }
  private boolean canProcessRequest(ServletRequest request,
  private boolean canProcessRequest(HttpServletRequest request,
      final HTTPClientConnection clientConnection) throws UnknownHostException
  {
    InetAddress clientAddr = InetAddress.getByName(request.getRemoteAddr());
@@ -415,20 +448,18 @@
   * @throws ResourceException
   *           if any error occur
   */
  String[] extractUsernamePassword(ServletRequest request)
  String[] extractUsernamePassword(HttpServletRequest request)
      throws ResourceException
  {
    HttpServletRequest req = (HttpServletRequest) request;
    // TODO Use session to reduce hits with search + bind?
    // Use proxied authorization control for session.
    if (authConfig.isCustomHeadersAuthenticationSupported())
    {
      final String userName =
          req.getHeader(authConfig.getCustomHeaderUsername());
          request.getHeader(authConfig.getCustomHeaderUsername());
      final String password =
          req.getHeader(authConfig.getCustomHeaderPassword());
          request.getHeader(authConfig.getCustomHeaderPassword());
      if (userName != null && password != null)
      {
        return new String[] { userName, password };
@@ -437,7 +468,7 @@
    if (authConfig.isBasicAuthenticationSupported())
    {
      String httpBasicAuthHeader = req.getHeader(HTTP_BASIC_AUTH_HEADER);
      String httpBasicAuthHeader = request.getHeader(HTTP_BASIC_AUTH_HEADER);
      if (httpBasicAuthHeader != null)
      {
        String[] userPassword = parseUsernamePassword(httpBasicAuthHeader);
@@ -463,26 +494,23 @@
   * @param re
   *          the resource exception with the error response content
   */
  void sendErrorReponse(ServletResponse response, boolean prettyPrint,
  void sendErrorReponse(HttpServletResponse response, boolean prettyPrint,
      ResourceException re)
  {
    HttpServletResponse resp = (HttpServletResponse) response;
    resp.setStatus(re.getCode());
    response.setStatus(re.getCode());
    if (re.getCode() == HttpServletResponse.SC_UNAUTHORIZED
        && authConfig.isBasicAuthenticationSupported())
    {
      resp.setHeader("WWW-Authenticate",
      response.setHeader("WWW-Authenticate",
          "Basic realm=\"org.forgerock.opendj\"");
    }
    try
    {
      // Send error JSON document out
      resp.setHeader("Content-Type", "application/json");
      ServletOutputStream out = resp.getOutputStream();
      out.println(toJSON(prettyPrint, re));
      response.setHeader("Content-Type", "application/json");
      response.getOutputStream().println(toJSON(prettyPrint, re));
    }
    catch (IOException ignore)
    {
opends/src/server/org/opends/server/protocols/http/SdkConnectionAdapter.java
@@ -33,6 +33,8 @@
import java.util.LinkedHashSet;
import java.util.concurrent.atomic.AtomicInteger;
import javax.servlet.http.HttpServletResponse;
import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
import org.forgerock.opendj.ldap.ConnectionEventListener;
import org.forgerock.opendj.ldap.ErrorResultException;
@@ -68,6 +70,7 @@
import org.opends.server.core.QueueingStrategy;
import org.opends.server.core.SearchOperationBasis;
import org.opends.server.core.UnbindOperationBasis;
import org.opends.server.loggers.HTTPRequestInfo;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.ByteString;
@@ -91,6 +94,9 @@
  /** The HTTP client connection being "adapted". */
  private final HTTPClientConnection clientConnection;
  /** The HTTP request information to log. */
  private final HTTPRequestInfo requestInfo;
  /**
   * The next message ID (and operation ID) that should be used for this
   * connection.
@@ -111,10 +117,14 @@
   *
   * @param clientConnection
   *          the HTTP client connection being "adapted"
   * @param requestInfo
   *          the HTTP request information to log
   */
  public SdkConnectionAdapter(HTTPClientConnection clientConnection)
  public SdkConnectionAdapter(HTTPClientConnection clientConnection,
      HTTPRequestInfo requestInfo)
  {
    this.clientConnection = clientConnection;
    this.requestInfo = requestInfo;
    this.queueingStrategy =
        new BoundedWorkQueueStrategy(clientConnection.getConnectionHandler()
            .getCurrentConfig().getMaxConcurrentOpsPerConnection());
@@ -223,6 +233,11 @@
    {
      this.clientConnection.disconnect(DisconnectReason.UNBIND, false, null);
    }
    // At this point, we try to log the request with OK status code.
    // If it was already logged, it will be a no op.
    this.requestInfo.log(HttpServletResponse.SC_OK);
    isClosed = true;
  }
opends/tests/unit-tests-testng/src/server/org/opends/server/TestCaseUtils.java
@@ -51,7 +51,14 @@
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.LogManager;
@@ -78,8 +85,10 @@
import org.opends.server.extensions.ConfigFileHandler;
import org.opends.server.loggers.AccessLogger;
import org.opends.server.loggers.ErrorLogger;
import org.opends.server.loggers.HTTPAccessLogger;
import org.opends.server.loggers.TextAccessLogPublisher;
import org.opends.server.loggers.TextErrorLogPublisher;
import org.opends.server.loggers.TextHTTPAccessLogPublisher;
import org.opends.server.loggers.debug.DebugLogger;
import org.opends.server.loggers.debug.TextDebugLogPublisher;
import org.opends.server.plugins.InvocationCounterPlugin;
@@ -187,23 +196,17 @@
  public static final String PROPERTY_OPENDMK_LOCATION =
          "org.opends.server.snmp.opendmk";
  /**
   * The test text writer for the Debug Logger
   */
  public static TestTextWriter DEBUG_TEXT_WRITER =
      new TestTextWriter();
  /** The test text writer for the Debug Logger */
  public static TestTextWriter DEBUG_TEXT_WRITER = new TestTextWriter();
  /**
   * The test text writer for the Debug Logger
   */
  public static TestTextWriter ERROR_TEXT_WRITER =
      new TestTextWriter();
  /** The test text writer for the Error Logger */
  public static TestTextWriter ERROR_TEXT_WRITER = new TestTextWriter();
  /**
   * The test text writer for the Debug Logger
   */
  public static TestTextWriter ACCESS_TEXT_WRITER =
      new TestTextWriter();
  /** The test text writer for the Access Logger */
  public static TestTextWriter ACCESS_TEXT_WRITER = new TestTextWriter();
  /** The test text writer for the HTTP Access Logger */
  public static TestTextWriter HTTP_ACCESS_TEXT_WRITER = new TestTextWriter();
  /**
   * Indicates whether the server has already been started.  The value of this
@@ -525,6 +528,9 @@
          TextAccessLogPublisher.getStartupTextAccessPublisher(
              ACCESS_TEXT_WRITER, false));
      HTTPAccessLogger.addHTTPAccessLogPublisher(TextHTTPAccessLogPublisher
          .getStartupTextHTTPAccessPublisher(HTTP_ACCESS_TEXT_WRITER));
      // Use more verbose tool logger.
      ErrorLogger.addErrorLogPublisher(
         TextErrorLogPublisher.getToolStartupTextErrorPublisher(