/*
* 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 legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* 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 legal-notices/CDDLv1_0.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 2013-2015 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.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.util.Utils;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.FileBasedHTTPAccessLogPublisherCfg;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ServerContext;
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.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>
{
// Extended log format standard fields
private static final String ELF_C_IP = "c-ip";
private static final String ELF_C_PORT = "c-port";
private static final String ELF_CS_HOST = "cs-host";
private static final String ELF_CS_METHOD = "cs-method";
private static final String ELF_CS_URI_QUERY = "cs-uri-query";
private static final String ELF_CS_USER_AGENT = "cs(User-Agent)";
private static final String ELF_CS_USERNAME = "cs-username";
private static final String ELF_CS_VERSION = "cs-version";
private static final String ELF_S_COMPUTERNAME = "s-computername";
private static final String ELF_S_IP = "s-ip";
private static final String ELF_S_PORT = "s-port";
private static final String ELF_SC_STATUS = "sc-status";
// Application specific fields (eXtensions)
private static final String X_CONNECTION_ID = "x-connection-id";
private static final String X_DATETIME = "x-datetime";
private static final String X_ETIME = "x-etime";
private static final String X_TRANSACTION_ID = "x-transaction-id";
private static final Set<String> ALL_SUPPORTED_FIELDS = new HashSet<>(
Arrays.asList(ELF_C_IP, ELF_C_PORT, ELF_CS_HOST, ELF_CS_METHOD,
ELF_CS_URI_QUERY, ELF_CS_USER_AGENT, ELF_CS_USERNAME, ELF_CS_VERSION,
ELF_S_COMPUTERNAME, ELF_S_IP, ELF_S_PORT, ELF_SC_STATUS,
X_CONNECTION_ID, X_DATETIME, X_ETIME, X_TRANSACTION_ID));
/**
* 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;
private FileBasedHTTPAccessLogPublisherCfg cfg;
private List<String> logFormatFields;
private String timeStampFormat = "dd/MMM/yyyy:HH:mm:ss Z";
@Override
public ConfigChangeResult applyConfigurationChange(final FileBasedHTTPAccessLogPublisherCfg config)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
try
{
// Determine the writer we are using. If we were writing asynchronously,
// we need to modify the underlying writer.
TextWriter currentWriter;
if (writer instanceof AsynchronousTextWriter)
{
currentWriter = ((AsynchronousTextWriter) writer).getWrappedWriter();
}
else
{
currentWriter = writer;
}
if (currentWriter instanceof MultifileTextWriter)
{
final MultifileTextWriter mfWriter = (MultifileTextWriter) currentWriter;
configure(mfWriter, config);
if (config.isAsynchronous())
{
if (writer instanceof AsynchronousTextWriter)
{
if (hasAsyncConfigChanged(config))
{
// reinstantiate
final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
writer = newAsyncWriter(mfWriter, config);
previousWriter.shutdown(false);
}
}
else
{
// turn async text writer on
writer = newAsyncWriter(mfWriter, config);
}
}
else
{
if (writer instanceof AsynchronousTextWriter)
{
// asynchronous is being turned off, remove async text writers.
final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
writer = mfWriter;
previousWriter.shutdown(false);
}
}
if (cfg.isAsynchronous() && config.isAsynchronous()
&& cfg.getQueueSize() != config.getQueueSize())
{
ccr.setAdminActionRequired(true);
}
if (!config.getLogRecordTimeFormat().equals(timeStampFormat))
{
TimeThread.removeUserDefinedFormatter(timeStampFormat);
timeStampFormat = config.getLogRecordTimeFormat();
}
cfg = config;
logFormatFields = extractFieldsOrder(cfg.getLogFormat());
LocalizableMessage errorMessage = validateLogFormat(logFormatFields);
if (errorMessage != null)
{
ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
ccr.setAdminActionRequired(true);
ccr.addMessage(errorMessage);
}
}
}
catch (final Exception e)
{
ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
config.dn(), stackTraceToSingleLineString(e)));
}
return ccr;
}
private void configure(MultifileTextWriter mfWriter, FileBasedHTTPAccessLogPublisherCfg config)
throws DirectoryException
{
final FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
final boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
final File logFile = getLogFile(config);
final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
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));
}
}
private File getLogFile(final FileBasedHTTPAccessLogPublisherCfg config)
{
return getFileForPath(config.getLogFile());
}
private boolean hasAsyncConfigChanged(FileBasedHTTPAccessLogPublisherCfg newConfig)
{
return hasParallelConfigChanged(newConfig) && cfg.getQueueSize() != newConfig.getQueueSize();
}
private boolean hasParallelConfigChanged(FileBasedHTTPAccessLogPublisherCfg newConfig)
{
return !cfg.dn().equals(newConfig.dn()) && cfg.isAutoFlush() != newConfig.isAutoFlush();
}
private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter mfWriter, FileBasedHTTPAccessLogPublisherCfg config)
{
String name = "Asynchronous Text Writer for " + config.dn();
return new AsynchronousTextWriter(name, config.getQueueSize(), config.isAutoFlush(), mfWriter);
}
private List<String> extractFieldsOrder(String logFormat)
{
// there will always be at least one field value due to the regexp
// validating the log format
return Arrays.asList(logFormat.split(" "));
}
/**
* Validates the provided fields for the log format.
*
* @param fields
* the fields comprising the log format.
* @return an error message when validation fails, null otherwise
*/
private LocalizableMessage validateLogFormat(List<String> fields)
{
final Collection<String> unsupportedFields =
subtract(fields, ALL_SUPPORTED_FIELDS);
if (!unsupportedFields.isEmpty())
{ // there are some unsupported fields. List them.
return WARN_CONFIG_LOGGING_UNSUPPORTED_FIELDS_IN_LOG_FORMAT.get(
cfg.dn(), Utils.joinAsString(", ", unsupportedFields));
}
if (fields.size() == unsupportedFields.size())
{ // all fields are unsupported
return ERR_CONFIG_LOGGING_EMPTY_LOG_FORMAT.get(cfg.dn());
}
return null;
}
/**
* Returns a new Collection containing a - b.
*
* @param <T>
* @param a
* the collection to subtract from, must not be null
* @param b
* the collection to subtract, must not be null
* @return a new collection with the results
*/
private <T> Collection<T> subtract(Collection<T> a, Collection<T> b)
{
final Collection<T> result = new ArrayList<>();
for (T elem : a)
{
if (!b.contains(elem))
{
result.add(elem);
}
}
return result;
}
@Override
public void initializeLogPublisher(
final FileBasedHTTPAccessLogPublisherCfg cfg, ServerContext serverContext)
throws ConfigException, InitializationException
{
final File logFile = getLogFile(cfg);
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(),
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())
{
this.writer = newAsyncWriter(theWriter, cfg);
}
else
{
this.writer = theWriter;
}
}
catch (final DirectoryException e)
{
throw new InitializationException(
ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg.dn(), e), e);
}
catch (final IOException e)
{
throw new InitializationException(
ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, cfg.dn(), e), e);
}
this.cfg = cfg;
logFormatFields = extractFieldsOrder(cfg.getLogFormat());
LocalizableMessage error = validateLogFormat(logFormatFields);
if (error != null)
{
throw new InitializationException(error);
}
timeStampFormat = cfg.getLogRecordTimeFormat();
cfg.addFileBasedHTTPAccessChangeListener(this);
}
@Override
public boolean isConfigurationAcceptable(
final FileBasedHTTPAccessLogPublisherCfg configuration,
final List<LocalizableMessage> unacceptableReasons)
{
return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
}
@Override
public boolean isConfigurationChangeAcceptable(
final FileBasedHTTPAccessLogPublisherCfg config,
final List<LocalizableMessage> unacceptableReasons)
{
// Validate the time-stamp formatter.
final String formatString = config.getLogRecordTimeFormat();
try
{
new SimpleDateFormat(formatString);
}
catch (final Exception e)
{
unacceptableReasons.add(ERR_CONFIG_LOGGING_INVALID_TIME_FORMAT.get(formatString));
return false;
}
// Make sure the permission is valid.
try
{
final FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
if (!filePerm.isOwnerWritable())
{
final LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions());
unacceptableReasons.add(message);
return false;
}
}
catch (final DirectoryException e)
{
unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
return false;
}
return true;
}
@Override
public final void close()
{
writer.shutdown();
TimeThread.removeUserDefinedFormatter(timeStampFormat);
if (cfg != null)
{
cfg.removeFileBasedHTTPAccessChangeListener(this);
}
}
@Override
public final DN getDN()
{
return cfg != null ? cfg.dn() : null;
}
@Override
public void logRequestInfo(HTTPRequestInfo ri)
{
final Map<String, Object> fields = new HashMap<>();
fields.put(ELF_C_IP, ri.getClientAddress());
fields.put(ELF_C_PORT, ri.getClientPort());
fields.put(ELF_CS_HOST, ri.getClientHost());
fields.put(ELF_CS_METHOD, ri.getMethod());
fields.put(ELF_CS_URI_QUERY, ri.getQuery());
fields.put(ELF_CS_USER_AGENT, ri.getUserAgent());
fields.put(ELF_CS_USERNAME, ri.getAuthUser());
fields.put(ELF_CS_VERSION, ri.getProtocol());
fields.put(ELF_S_IP, ri.getServerAddress());
fields.put(ELF_S_COMPUTERNAME, ri.getServerHost());
fields.put(ELF_S_PORT, ri.getServerPort());
fields.put(ELF_SC_STATUS, ri.getStatusCode());
fields.put(X_CONNECTION_ID, ri.getConnectionID());
fields.put(X_DATETIME, TimeThread.getUserDefinedTime(timeStampFormat));
fields.put(X_ETIME, ri.getTotalProcessingTime());
fields.put(X_TRANSACTION_ID, ri.getTransactionId());
writeLogRecord(fields, logFormatFields);
}
private void writeLogRecord(Map<String, Object> fields,
List<String> fieldnames)
{
if (fieldnames == null)
{
return;
}
final StringBuilder sb = new StringBuilder(100);
for (String fieldname : fieldnames)
{
append(sb, fields.get(fieldname));
}
writer.writeRecord(sb.toString());
}
/**
* Appends the value to the string builder using the default separator if needed.
*
* @param sb
* the StringBuilder where to append.
* @param value
* the value to append.
*/
private void append(final StringBuilder sb, Object value)
{
final char separator = '\t'; // as encouraged by the W3C working draft
if (sb.length() > 0)
{
sb.append(separator);
}
if (value != null)
{
String val = String.valueOf(value);
boolean useQuotes = val.contains(Character.toString(separator));
if (useQuotes)
{
sb.append('"').append(val.replaceAll("\"", "\"\"")).append('"');
}
else
{
sb.append(val);
}
}
else
{
sb.append('-');
}
}
}