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