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