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