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.ConfigException; 045import org.forgerock.util.Utils; 046import org.opends.server.admin.server.ConfigurationChangeListener; 047import org.opends.server.admin.std.server.FileBasedHTTPAccessLogPublisherCfg; 048import org.opends.server.core.DirectoryServer; 049import org.opends.server.core.ServerContext; 050import org.forgerock.opendj.config.server.ConfigChangeResult; 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 084 private static final Set<String> ALL_SUPPORTED_FIELDS = new HashSet<>( 085 Arrays.asList(ELF_C_IP, ELF_C_PORT, ELF_CS_HOST, ELF_CS_METHOD, 086 ELF_CS_URI_QUERY, ELF_CS_USER_AGENT, ELF_CS_USERNAME, ELF_CS_VERSION, 087 ELF_S_COMPUTERNAME, ELF_S_IP, ELF_S_PORT, ELF_SC_STATUS, 088 X_CONNECTION_ID, X_DATETIME, X_ETIME)); 089 090 /** 091 * Returns an instance of the text HTTP access log publisher that will print 092 * all messages to the provided writer. This is used to print the messages to 093 * the console when the server starts up. 094 * 095 * @param writer 096 * The text writer where the message will be written to. 097 * @return The instance of the text error log publisher that will print all 098 * messages to standard out. 099 */ 100 public static TextHTTPAccessLogPublisher getStartupTextHTTPAccessPublisher( 101 final TextWriter writer) 102 { 103 final TextHTTPAccessLogPublisher startupPublisher = 104 new TextHTTPAccessLogPublisher(); 105 startupPublisher.writer = writer; 106 return startupPublisher; 107 } 108 109 110 111 private TextWriter writer; 112 private FileBasedHTTPAccessLogPublisherCfg cfg; 113 private List<String> logFormatFields; 114 private String timeStampFormat = "dd/MMM/yyyy:HH:mm:ss Z"; 115 116 117 /** {@inheritDoc} */ 118 @Override 119 public ConfigChangeResult applyConfigurationChange( 120 final FileBasedHTTPAccessLogPublisherCfg config) 121 { 122 final ConfigChangeResult ccr = new ConfigChangeResult(); 123 124 final File logFile = getFileForPath(config.getLogFile()); 125 final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile); 126 try 127 { 128 final FilePermission perm = FilePermission.decodeUNIXMode(config 129 .getLogFilePermissions()); 130 131 final boolean writerAutoFlush = config.isAutoFlush() 132 && !config.isAsynchronous(); 133 134 TextWriter currentWriter; 135 // Determine the writer we are using. If we were writing 136 // asynchronously, we need to modify the underlying writer. 137 if (writer instanceof AsynchronousTextWriter) 138 { 139 currentWriter = ((AsynchronousTextWriter) writer).getWrappedWriter(); 140 } 141 else if (writer instanceof ParallelTextWriter) 142 { 143 currentWriter = ((ParallelTextWriter) writer).getWrappedWriter(); 144 } 145 else 146 { 147 currentWriter = writer; 148 } 149 150 if (currentWriter instanceof MultifileTextWriter) 151 { 152 final MultifileTextWriter mfWriter = 153 (MultifileTextWriter) currentWriter; 154 155 mfWriter.setNamingPolicy(fnPolicy); 156 mfWriter.setFilePermissions(perm); 157 mfWriter.setAppend(config.isAppend()); 158 mfWriter.setAutoFlush(writerAutoFlush); 159 mfWriter.setBufferSize((int) config.getBufferSize()); 160 mfWriter.setInterval(config.getTimeInterval()); 161 162 mfWriter.removeAllRetentionPolicies(); 163 mfWriter.removeAllRotationPolicies(); 164 165 for (final DN dn : config.getRotationPolicyDNs()) 166 { 167 mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn)); 168 } 169 170 for (final DN dn : config.getRetentionPolicyDNs()) 171 { 172 mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn)); 173 } 174 175 if (writer instanceof AsynchronousTextWriter 176 && !config.isAsynchronous()) 177 { 178 // The asynchronous setting is being turned off. 179 final AsynchronousTextWriter asyncWriter = 180 (AsynchronousTextWriter) writer; 181 writer = mfWriter; 182 asyncWriter.shutdown(false); 183 } 184 185 if (writer instanceof ParallelTextWriter && !config.isAsynchronous()) 186 { 187 // The asynchronous setting is being turned off. 188 final ParallelTextWriter asyncWriter = (ParallelTextWriter) writer; 189 writer = mfWriter; 190 asyncWriter.shutdown(false); 191 } 192 193 if (!(writer instanceof AsynchronousTextWriter) 194 && config.isAsynchronous()) 195 { 196 // The asynchronous setting is being turned on. 197 final AsynchronousTextWriter asyncWriter = new AsynchronousTextWriter( 198 "Asynchronous Text Writer for " + config.dn(), 199 config.getQueueSize(), config.isAutoFlush(), mfWriter); 200 writer = asyncWriter; 201 } 202 203 if (!(writer instanceof ParallelTextWriter) && config.isAsynchronous()) 204 { 205 // The asynchronous setting is being turned on. 206 final ParallelTextWriter asyncWriter = new ParallelTextWriter( 207 "Parallel Text Writer for " + config.dn(), 208 config.isAutoFlush(), mfWriter); 209 writer = asyncWriter; 210 } 211 212 if (cfg.isAsynchronous() && config.isAsynchronous() 213 && cfg.getQueueSize() != config.getQueueSize()) 214 { 215 ccr.setAdminActionRequired(true); 216 } 217 218 if (!config.getLogRecordTimeFormat().equals(timeStampFormat)) 219 { 220 TimeThread.removeUserDefinedFormatter(timeStampFormat); 221 timeStampFormat = config.getLogRecordTimeFormat(); 222 } 223 224 cfg = config; 225 logFormatFields = extractFieldsOrder(cfg.getLogFormat()); 226 LocalizableMessage errorMessage = validateLogFormat(logFormatFields); 227 if (errorMessage != null) 228 { 229 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 230 ccr.setAdminActionRequired(true); 231 ccr.addMessage(errorMessage); 232 } 233 } 234 } 235 catch (final Exception e) 236 { 237 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 238 ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get( 239 config.dn(), stackTraceToSingleLineString(e))); 240 } 241 242 return ccr; 243 } 244 245 246 private List<String> extractFieldsOrder(String logFormat) 247 { 248 // there will always be at least one field value due to the regexp 249 // validating the log format 250 return Arrays.asList(logFormat.split(" ")); 251 } 252 253 /** 254 * Validates the provided fields for the log format. 255 * 256 * @param fields 257 * the fields comprising the log format. 258 * @return an error message when validation fails, null otherwise 259 */ 260 private LocalizableMessage validateLogFormat(List<String> fields) 261 { 262 final Collection<String> unsupportedFields = 263 subtract(fields, ALL_SUPPORTED_FIELDS); 264 if (!unsupportedFields.isEmpty()) 265 { // there are some unsupported fields. List them. 266 return WARN_CONFIG_LOGGING_UNSUPPORTED_FIELDS_IN_LOG_FORMAT.get( 267 cfg.dn(), Utils.joinAsString(", ", unsupportedFields)); 268 } 269 if (fields.size() == unsupportedFields.size()) 270 { // all fields are unsupported 271 return ERR_CONFIG_LOGGING_EMPTY_LOG_FORMAT.get(cfg.dn()); 272 } 273 return null; 274 } 275 276 /** 277 * Returns a new Collection containing a - b. 278 * 279 * @param <T> 280 * @param a 281 * the collection to subtract from, must not be null 282 * @param b 283 * the collection to subtract, must not be null 284 * @return a new collection with the results 285 */ 286 private <T> Collection<T> subtract(Collection<T> a, Collection<T> b) 287 { 288 final Collection<T> result = new ArrayList<>(); 289 for (T elem : a) 290 { 291 if (!b.contains(elem)) 292 { 293 result.add(elem); 294 } 295 } 296 return result; 297 } 298 299 /** {@inheritDoc} */ 300 @Override 301 public void initializeLogPublisher( 302 final FileBasedHTTPAccessLogPublisherCfg cfg, ServerContext serverContext) 303 throws ConfigException, InitializationException 304 { 305 final File logFile = getFileForPath(cfg.getLogFile()); 306 final FileNamingPolicy fnPolicy = new TimeStampNaming(logFile); 307 308 try 309 { 310 final FilePermission perm = FilePermission.decodeUNIXMode(cfg 311 .getLogFilePermissions()); 312 313 final LogPublisherErrorHandler errorHandler = 314 new LogPublisherErrorHandler(cfg.dn()); 315 316 final boolean writerAutoFlush = cfg.isAutoFlush() 317 && !cfg.isAsynchronous(); 318 319 final MultifileTextWriter theWriter = new MultifileTextWriter( 320 "Multifile Text Writer for " + cfg.dn(), 321 cfg.getTimeInterval(), fnPolicy, perm, errorHandler, "UTF-8", 322 writerAutoFlush, cfg.isAppend(), (int) cfg.getBufferSize()); 323 324 // Validate retention and rotation policies. 325 for (final DN dn : cfg.getRotationPolicyDNs()) 326 { 327 theWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn)); 328 } 329 330 for (final DN dn : cfg.getRetentionPolicyDNs()) 331 { 332 theWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn)); 333 } 334 335 if (cfg.isAsynchronous()) 336 { 337 if (cfg.getQueueSize() > 0) 338 { 339 this.writer = new AsynchronousTextWriter("Asynchronous Text Writer for " + cfg.dn(), 340 cfg.getQueueSize(), cfg.isAutoFlush(), theWriter); 341 } 342 else 343 { 344 this.writer = new ParallelTextWriter("Parallel Text Writer for " + cfg.dn(), 345 cfg.isAutoFlush(), theWriter); 346 } 347 } 348 else 349 { 350 this.writer = theWriter; 351 } 352 } 353 catch (final DirectoryException e) 354 { 355 throw new InitializationException( 356 ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(cfg.dn(), e), e); 357 } 358 catch (final IOException e) 359 { 360 throw new InitializationException( 361 ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, cfg.dn(), e), e); 362 } 363 364 this.cfg = cfg; 365 logFormatFields = extractFieldsOrder(cfg.getLogFormat()); 366 LocalizableMessage error = validateLogFormat(logFormatFields); 367 if (error != null) 368 { 369 throw new InitializationException(error); 370 } 371 timeStampFormat = cfg.getLogRecordTimeFormat(); 372 373 cfg.addFileBasedHTTPAccessChangeListener(this); 374 } 375 376 377 /** {@inheritDoc} */ 378 @Override 379 public boolean isConfigurationAcceptable( 380 final FileBasedHTTPAccessLogPublisherCfg configuration, 381 final List<LocalizableMessage> unacceptableReasons) 382 { 383 return isConfigurationChangeAcceptable(configuration, unacceptableReasons); 384 } 385 386 387 /** {@inheritDoc} */ 388 @Override 389 public boolean isConfigurationChangeAcceptable( 390 final FileBasedHTTPAccessLogPublisherCfg config, 391 final List<LocalizableMessage> unacceptableReasons) 392 { 393 // Validate the time-stamp formatter. 394 final String formatString = config.getLogRecordTimeFormat(); 395 try 396 { 397 new SimpleDateFormat(formatString); 398 } 399 catch (final Exception e) 400 { 401 unacceptableReasons.add(ERR_CONFIG_LOGGING_INVALID_TIME_FORMAT.get(formatString)); 402 return false; 403 } 404 405 // Make sure the permission is valid. 406 try 407 { 408 final FilePermission filePerm = FilePermission.decodeUNIXMode(config 409 .getLogFilePermissions()); 410 if (!filePerm.isOwnerWritable()) 411 { 412 final LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config 413 .getLogFilePermissions()); 414 unacceptableReasons.add(message); 415 return false; 416 } 417 } 418 catch (final DirectoryException e) 419 { 420 unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e)); 421 return false; 422 } 423 424 return true; 425 } 426 427 428 /** {@inheritDoc} */ 429 @Override 430 public final void close() 431 { 432 writer.shutdown(); 433 TimeThread.removeUserDefinedFormatter(timeStampFormat); 434 if (cfg != null) 435 { 436 cfg.removeFileBasedHTTPAccessChangeListener(this); 437 } 438 } 439 440 /** {@inheritDoc} */ 441 @Override 442 public final DN getDN() 443 { 444 return cfg != null ? cfg.dn() : null; 445 } 446 447 /** {@inheritDoc} */ 448 @Override 449 public void logRequestInfo(HTTPRequestInfo ri) 450 { 451 final Map<String, Object> fields = new HashMap<>(); 452 fields.put(ELF_C_IP, ri.getClientAddress()); 453 fields.put(ELF_C_PORT, ri.getClientPort()); 454 fields.put(ELF_CS_HOST, ri.getClientHost()); 455 fields.put(ELF_CS_METHOD, ri.getMethod()); 456 fields.put(ELF_CS_URI_QUERY, ri.getQuery()); 457 fields.put(ELF_CS_USER_AGENT, ri.getUserAgent()); 458 fields.put(ELF_CS_USERNAME, ri.getAuthUser()); 459 fields.put(ELF_CS_VERSION, ri.getProtocol()); 460 fields.put(ELF_S_IP, ri.getServerAddress()); 461 fields.put(ELF_S_COMPUTERNAME, ri.getServerHost()); 462 fields.put(ELF_S_PORT, ri.getServerPort()); 463 fields.put(ELF_SC_STATUS, ri.getStatusCode()); 464 fields.put(X_CONNECTION_ID, ri.getConnectionID()); 465 fields.put(X_DATETIME, TimeThread.getUserDefinedTime(timeStampFormat)); 466 fields.put(X_ETIME, ri.getTotalProcessingTime()); 467 468 writeLogRecord(fields, logFormatFields); 469 } 470 471 private void writeLogRecord(Map<String, Object> fields, 472 List<String> fieldnames) 473 { 474 if (fieldnames == null) 475 { 476 return; 477 } 478 final StringBuilder sb = new StringBuilder(100); 479 for (String fieldname : fieldnames) 480 { 481 append(sb, fields.get(fieldname)); 482 } 483 writer.writeRecord(sb.toString()); 484 } 485 486 /** 487 * Appends the value to the string builder using the default separator if 488 * needed. 489 * 490 * @param sb 491 * the StringBuilder where to append. 492 * @param value 493 * the value to append. 494 */ 495 private void append(final StringBuilder sb, Object value) 496 { 497 final char separator = '\t'; // as encouraged by the W3C working draft 498 if (sb.length() > 0) 499 { 500 sb.append(separator); 501 } 502 503 if (value != null) 504 { 505 String val = String.valueOf(value); 506 boolean useQuotes = val.contains(Character.toString(separator)); 507 if (useQuotes) 508 { 509 sb.append('"').append(val.replaceAll("\"", "\"\"")).append('"'); 510 } 511 else 512 { 513 sb.append(val); 514 } 515 } 516 else 517 { 518 sb.append('-'); 519 } 520 } 521 522}