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 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.loggers; 028 029 030 import org.opends.server.api.DirectoryThread; 031 import org.opends.server.api.ServerShutdownListener; 032 import org.opends.server.core.DirectoryServer; 033 import org.opends.server.types.*; 034 import org.opends.server.types.FilePermission; 035 import org.opends.server.admin.std.server.SizeLimitLogRotationPolicyCfg; 036 import org.opends.server.admin.server.ConfigurationChangeListener; 037 import org.opends.server.util.TimeThread; 038 import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; 039 040 import static org.opends.server.loggers.debug.DebugLogger.*; 041 import org.opends.server.loggers.debug.DebugTracer; 042 import static org.opends.messages.LoggerMessages.*; 043 import org.opends.messages.Message; 044 045 import java.io.*; 046 import java.util.concurrent.CopyOnWriteArrayList; 047 import java.util.ArrayList; 048 import java.util.List; 049 import java.util.Calendar; 050 051 /** 052 * A MultiFileTextWriter is a specialized TextWriter which supports publishing 053 * log records to a set of files. MultiFileWriters write to one file in the 054 * set at a time, switching files as is dictated by a specified rotation 055 * and retention policies. 056 * 057 * When a switch is required, the writer closes the current file and opens a 058 * new one named in accordance with a specified FileNamingPolicy. 059 */ 060 public class MultifileTextWriter 061 implements ServerShutdownListener, TextWriter, 062 ConfigurationChangeListener<SizeLimitLogRotationPolicyCfg> 063 { 064 /** 065 * The tracer object for the debug logger. 066 */ 067 private static final DebugTracer TRACER = getTracer(); 068 069 private static final String UTF8_ENCODING= "UTF-8"; 070 071 private CopyOnWriteArrayList<RotationPolicy> rotationPolicies = 072 new CopyOnWriteArrayList<RotationPolicy>(); 073 private CopyOnWriteArrayList<RetentionPolicy> retentionPolicies = 074 new CopyOnWriteArrayList<RetentionPolicy>(); 075 076 private FileNamingPolicy namingPolicy; 077 private FilePermission filePermissions; 078 private LogPublisherErrorHandler errorHandler; 079 //TODO: Implement actions. 080 private ArrayList<ActionType> actions; 081 082 private String name; 083 private String encoding; 084 private int bufferSize; 085 private boolean autoFlush; 086 private boolean append; 087 private long interval; 088 private boolean stopRequested; 089 private long sizeLimit = 0; 090 091 private Thread rotaterThread; 092 093 private Calendar lastRotationTime = TimeThread.getCalendar(); 094 private Calendar lastCleanTime = TimeThread.getCalendar(); 095 private long lastCleanCount = 0; 096 private long totalFilesRotated = 0; 097 private long totalFilesCleaned = 0; 098 099 /** The underlying output stream. */ 100 private MeteredStream outputStream; 101 /** The underlaying buffered writer using the output steram. */ 102 private BufferedWriter writer; 103 104 /** 105 * Creates a new instance of MultiFileTextWriter with the supplied policies. 106 * 107 * @param name the name of the log rotation thread. 108 * @param interval the interval to check whether the logs need to be rotated. 109 * @param namingPolicy the file naming policy to use to name rotated log. 110 * files. 111 * @param filePermissions the file permissions to set on the log files. 112 * @param errorHandler the log publisher error handler to notify when 113 * an error occurs. 114 * @param encoding the encoding to use to write the log files. 115 * @param autoFlush whether to flush the writer on every println. 116 * @param append whether to append to an existing log file. 117 * @param bufferSize the bufferSize to use for the writer. 118 * @throws IOException if an error occurs while creating the log file. 119 * @throws DirectoryException if an error occurs while preping the new log 120 * file. 121 */ 122 public MultifileTextWriter(String name, long interval, 123 FileNamingPolicy namingPolicy, 124 FilePermission filePermissions, 125 LogPublisherErrorHandler errorHandler, 126 String encoding, 127 boolean autoFlush, 128 boolean append, 129 int bufferSize) 130 throws IOException, DirectoryException 131 { 132 File file = namingPolicy.getInitialName(); 133 constructWriter(file, filePermissions, encoding, append, 134 bufferSize); 135 136 this.name = name; 137 this.interval = interval; 138 this.namingPolicy = namingPolicy; 139 this.filePermissions = filePermissions; 140 this.errorHandler = errorHandler; 141 142 this.encoding = UTF8_ENCODING; 143 this.autoFlush = autoFlush; 144 this.append = append; 145 this.bufferSize = bufferSize; 146 147 this.stopRequested = false; 148 149 rotaterThread = new RotaterThread(this); 150 rotaterThread.start(); 151 152 DirectoryServer.registerShutdownListener(this); 153 } 154 155 /** 156 * Construct a PrintWriter for a file. 157 * @param file - the file to open for writing 158 * @param filePermissions - the file permissions to set on the file. 159 * @param encoding - the encoding to use when writing log records. 160 * @param append - indicates whether the file should be appended to or 161 * truncated. 162 * @param bufferSize - the buffer size to use for the writer. 163 * @throws IOException if the PrintWriter could not be constructed 164 * or if the file already exists and it was indicated this should be 165 * an error. 166 * @throws DirectoryException if there was a problem setting permissions on 167 * the file. 168 */ 169 private void constructWriter(File file, FilePermission filePermissions, 170 String encoding, boolean append, 171 int bufferSize) 172 throws IOException, DirectoryException 173 { 174 // Create new file if it doesn't exist 175 if(!file.exists()) 176 { 177 file.createNewFile(); 178 } 179 180 FileOutputStream stream = new FileOutputStream(file, append); 181 outputStream = new MeteredStream(stream, file.length()); 182 183 OutputStreamWriter osw = new OutputStreamWriter(outputStream, encoding); 184 BufferedWriter bw = null; 185 if(bufferSize <= 0) 186 { 187 writer = new BufferedWriter(osw); 188 } 189 else 190 { 191 writer = new BufferedWriter(osw, bufferSize); 192 } 193 194 195 // Try to apply file permissions. 196 if(FilePermission.canSetPermissions()) 197 { 198 try 199 { 200 if(!FilePermission.setPermissions(file, filePermissions)) 201 { 202 Message message = WARN_LOGGER_UNABLE_SET_PERMISSIONS.get( 203 filePermissions.toString(), file.toString()); 204 ErrorLogger.logError(message); 205 } 206 } 207 catch(Exception e) 208 { 209 // Log an warning that the permissions were not set. 210 Message message = WARN_LOGGER_SET_PERMISSION_FAILED.get( 211 file.toString(), stackTraceToSingleLineString(e)); 212 ErrorLogger.logError(message); 213 } 214 } 215 } 216 217 218 /** 219 * Add a rotation policy to enforce on the files written by this writer. 220 * 221 * @param policy The rotation policy to add. 222 */ 223 public void addRotationPolicy(RotationPolicy policy) 224 { 225 this.rotationPolicies.add(policy); 226 227 if(policy instanceof SizeBasedRotationPolicy) 228 { 229 SizeBasedRotationPolicy sizePolicy = ((SizeBasedRotationPolicy)policy); 230 if(sizeLimit == 0 || 231 sizeLimit > sizePolicy.currentConfig.getFileSizeLimit()) 232 { 233 sizeLimit = sizePolicy.currentConfig.getFileSizeLimit(); 234 } 235 // Add this as a change listener so we can update the size limit. 236 sizePolicy.currentConfig.addSizeLimitChangeListener(this); 237 } 238 } 239 240 /** 241 * Add a retention policy to enforce on the files written by this writer. 242 * 243 * @param policy The retention policy to add. 244 */ 245 public void addRetentionPolicy(RetentionPolicy policy) 246 { 247 this.retentionPolicies.add(policy); 248 } 249 250 /** 251 * Removes all the rotation policies currently enforced by this writer. 252 */ 253 public void removeAllRotationPolicies() 254 { 255 for(RotationPolicy policy : rotationPolicies) 256 { 257 if(policy instanceof SizeBasedRotationPolicy) 258 { 259 sizeLimit = 0; 260 261 // Remove this as a change listener. 262 SizeBasedRotationPolicy sizePolicy = ((SizeBasedRotationPolicy)policy); 263 sizePolicy.currentConfig.removeSizeLimitChangeListener(this); 264 } 265 } 266 267 this.rotationPolicies.clear(); 268 } 269 270 /** 271 * Removes all retention policies being enforced by this writer. 272 */ 273 public void removeAllRetentionPolicies() 274 { 275 this.retentionPolicies.clear(); 276 } 277 278 /** 279 * Set the auto flush setting for this writer. 280 * 281 * @param autoFlush If the writer should flush the buffer after every line. 282 */ 283 public void setAutoFlush(boolean autoFlush) 284 { 285 this.autoFlush = autoFlush; 286 } 287 288 /** 289 * Set the append setting for this writter. 290 * 291 * @param append If the writer should append to an existing file. 292 */ 293 public void setAppend(boolean append) 294 { 295 this.append = append; 296 } 297 298 /** 299 * Set the buffer size for this writter. 300 * 301 * @param bufferSize The size of the underlying output stream buffer. 302 */ 303 public void setBufferSize(int bufferSize) 304 { 305 this.bufferSize = bufferSize; 306 } 307 308 /** 309 * Set the file permission to set for newly created log files. 310 * 311 * @param filePermissions The file permission to set for new log files. 312 */ 313 public void setFilePermissions(FilePermission filePermissions) 314 { 315 this.filePermissions = filePermissions; 316 } 317 318 /** 319 * Retrieves the current naming policy used to generate log file names. 320 * 321 * @return The current naming policy in use. 322 */ 323 public FileNamingPolicy getNamingPolicy() 324 { 325 return namingPolicy; 326 } 327 328 /** 329 * Set the naming policy to use when generating new log files. 330 * 331 * @param namingPolicy the naming policy to use to name log files. 332 */ 333 public void setNamingPolicy(FileNamingPolicy namingPolicy) 334 { 335 this.namingPolicy = namingPolicy; 336 } 337 338 /** 339 * Set the internval in which the rotator thread checks to see if the log 340 * file should be rotated. 341 * 342 * @param interval The interval to check if the log file needs to be rotated. 343 */ 344 public void setInterval(long interval) 345 { 346 this.interval = interval; 347 348 // Wake up the thread if its sleeping on the old interval 349 if(rotaterThread.getState() == Thread.State.TIMED_WAITING) 350 { 351 rotaterThread.interrupt(); 352 } 353 } 354 355 /** 356 * {@inheritDoc} 357 */ 358 public boolean isConfigurationChangeAcceptable( 359 SizeLimitLogRotationPolicyCfg config, List<Message> unacceptableReasons) 360 { 361 // This should always be ok 362 return true; 363 } 364 365 /** 366 * {@inheritDoc} 367 */ 368 public ConfigChangeResult applyConfigurationChange( 369 SizeLimitLogRotationPolicyCfg config) 370 { 371 if(sizeLimit == 0 || sizeLimit > config.getFileSizeLimit()) 372 { 373 sizeLimit = config.getFileSizeLimit(); 374 } 375 376 return new ConfigChangeResult(ResultCode.SUCCESS, false, 377 new ArrayList<Message>()); 378 } 379 380 /** 381 * A rotater thread is responsible for checking if the log files need to be 382 * rotated based on the policies. It will do so if necessary. 383 */ 384 private class RotaterThread extends DirectoryThread 385 { 386 MultifileTextWriter writer; 387 /** 388 * Create a new rotater thread. 389 */ 390 public RotaterThread(MultifileTextWriter writer) 391 { 392 super(name); 393 this.writer = writer; 394 } 395 396 /** 397 * the run method of the rotaterThread. It wakes up periodically and checks 398 * whether the file needs to be rotated based on the rotation policy. 399 */ 400 public void run() 401 { 402 while(!isShuttingDown()) 403 { 404 try 405 { 406 sleep(interval); 407 } 408 catch(InterruptedException e) 409 { 410 // We expect this to happen. 411 } 412 catch(Exception e) 413 { 414 if (debugEnabled()) 415 { 416 TRACER.debugCaught(DebugLogLevel.ERROR, e); 417 } 418 } 419 420 for(RotationPolicy rotationPolicy : rotationPolicies) 421 { 422 if(rotationPolicy.rotateFile(writer)) 423 { 424 rotate(); 425 } 426 } 427 428 for(RetentionPolicy retentionPolicy : retentionPolicies) 429 { 430 try 431 { 432 File[] files = 433 retentionPolicy.deleteFiles(writer.getNamingPolicy()); 434 435 for(File file : files) 436 { 437 file.delete(); 438 totalFilesCleaned++; 439 if(debugEnabled()) 440 { 441 TRACER.debugInfo(retentionPolicy.toString() + 442 " cleaned up log file %s", file.toString()); 443 } 444 } 445 446 if(files.length > 0) 447 { 448 lastCleanTime = TimeThread.getCalendar(); 449 lastCleanCount = files.length; 450 } 451 } 452 catch(DirectoryException de) 453 { 454 if(debugEnabled()) 455 { 456 TRACER.debugCaught(DebugLogLevel.ERROR, de); 457 } 458 errorHandler.handleDeleteError(retentionPolicy, de); 459 } 460 } 461 } 462 } 463 } 464 465 /** 466 * Retrieves the human-readable name for this shutdown listener. 467 * 468 * @return The human-readable name for this shutdown listener. 469 */ 470 public String getShutdownListenerName() 471 { 472 return "MultifileTextWriter Thread " + name; 473 } 474 475 /** 476 * Indicates that the Directory Server has received a request to stop running 477 * and that this shutdown listener should take any action necessary to prepare 478 * for it. 479 * 480 * @param reason The human-readable reason for the shutdown. 481 */ 482 public void processServerShutdown(Message reason) 483 { 484 stopRequested = true; 485 486 // Wait for rotater to terminate 487 while (rotaterThread != null && rotaterThread.isAlive()) { 488 try { 489 // Interrupt if its sleeping 490 rotaterThread.interrupt(); 491 rotaterThread.join(); 492 } 493 catch (InterruptedException ex) { 494 // Ignore; we gotta wait.. 495 } 496 } 497 498 DirectoryServer.deregisterShutdownListener(this); 499 500 removeAllRotationPolicies(); 501 removeAllRetentionPolicies(); 502 503 // Don't close the writer as there might still be message to be 504 // written. manually shutdown just before the server process 505 // exists. 506 } 507 508 /** 509 * Queries whether the publisher is in shutdown mode. 510 * 511 * @return if the publish is in shutdown mode. 512 */ 513 private boolean isShuttingDown() 514 { 515 return stopRequested; 516 } 517 518 /** 519 * Shutdown the text writer. 520 */ 521 public void shutdown() 522 { 523 processServerShutdown(null); 524 525 try 526 { 527 writer.flush(); 528 writer.close(); 529 } 530 catch(Exception e) 531 { 532 errorHandler.handleCloseError(e); 533 } 534 } 535 536 537 /** 538 * Write a log record string to the file. 539 * 540 * @param record the log record to write. 541 */ 542 public void writeRecord(String record) 543 { 544 // Assume each character is 1 byte ASCII 545 int length = record.length(); 546 int size = length; 547 char c; 548 for (int i=0; i < length; i++) 549 { 550 c = record.charAt(i); 551 if (c != (byte) (c & 0x0000007F)) 552 { 553 try 554 { 555 // String contains a non ASCII character. Fall back to getBytes. 556 size = record.getBytes("UTF-8").length; 557 } 558 catch(Exception e) 559 { 560 size = length * 2; 561 } 562 break; 563 } 564 } 565 566 synchronized(this) 567 { 568 if(sizeLimit > 0 && outputStream.written + size + 1 >= sizeLimit) 569 { 570 rotate(); 571 } 572 573 try 574 { 575 writer.write(record); 576 writer.newLine(); 577 } 578 catch(Exception e) 579 { 580 errorHandler.handleWriteError(record, e); 581 } 582 583 if(autoFlush) 584 { 585 flush(); 586 } 587 } 588 } 589 590 /** 591 * {@inheritDoc} 592 */ 593 public void flush() 594 { 595 try 596 { 597 writer.flush(); 598 } 599 catch(Exception e) 600 { 601 errorHandler.handleFlushError(e); 602 } 603 } 604 605 /** 606 * Tries to rotate the log files. If the new log file already exists, it 607 * tries to rename the file. On failure, all subsequent log write requests 608 * will throw exceptions. 609 */ 610 public synchronized void rotate() 611 { 612 try 613 { 614 writer.flush(); 615 writer.close(); 616 } 617 catch(Exception e) 618 { 619 if(debugEnabled()) 620 { 621 TRACER.debugCaught(DebugLogLevel.ERROR, e); 622 } 623 errorHandler.handleCloseError(e); 624 } 625 626 File currentFile = namingPolicy.getInitialName(); 627 File newFile = namingPolicy.getNextName(); 628 currentFile.renameTo(newFile); 629 630 try 631 { 632 constructWriter(currentFile, filePermissions, encoding, append, 633 bufferSize); 634 } 635 catch (Exception e) 636 { 637 if(debugEnabled()) 638 { 639 TRACER.debugCaught(DebugLogLevel.ERROR, e); 640 } 641 errorHandler.handleOpenError(currentFile, e); 642 } 643 644 //RotationActionThread rotThread = 645 // new RotationActionThread(newFile, actions, configEntry); 646 //rotThread.start(); 647 648 if(debugEnabled()) 649 { 650 TRACER.debugInfo("Log file %s rotated and renamed to %s", 651 currentFile, newFile); 652 } 653 654 totalFilesRotated++; 655 lastRotationTime = TimeThread.getCalendar(); 656 } 657 658 /** 659 * This method sets the actions that need to be executed after rotation. 660 * 661 * @param actions An array of actions that need to be executed on rotation. 662 */ 663 public void setPostRotationActions(ArrayList<ActionType> actions) 664 { 665 this.actions = actions; 666 } 667 668 /** 669 * Retrieves the number of bytes written to the current log file. 670 * 671 * @return The number of bytes written to the current log file. 672 */ 673 public long getBytesWritten() 674 { 675 return outputStream.written; 676 } 677 678 /** 679 * Retrieves the last time one or more log files are cleaned in this instance 680 * of the Directory Server. If log files have never been cleaned, this value 681 * will be the time the server started. 682 * 683 * @return The last time log files are cleaned. 684 */ 685 public Calendar getLastCleanTime() 686 { 687 return lastCleanTime; 688 } 689 690 /** 691 * Retrieves the number of files cleaned in the last cleanup run. 692 * 693 * @return The number of files cleaned int he last cleanup run. 694 */ 695 public long getLastCleanCount() 696 { 697 return lastCleanCount; 698 } 699 700 /** 701 * Retrieves the last time a log file was rotated in this instance of 702 * Directory Server. If a log rotation never 703 * occurred, this value will be the time the server started. 704 * 705 * @return The last time log rotation occurred. 706 */ 707 public Calendar getLastRotationTime() 708 { 709 return lastRotationTime; 710 } 711 712 /** 713 * Retrieves the total number file rotations occurred in this instance of the 714 * Directory Server. 715 * 716 * @return The total number of file rotations. 717 */ 718 public long getTotalFilesRotated() 719 { 720 return totalFilesRotated; 721 } 722 723 /** 724 * Retrieves the total number of files cleaned in this instance of the 725 * Directory Server. 726 * 727 * @return The total number of files cleaned. 728 */ 729 public long getTotalFilesCleaned() 730 { 731 return totalFilesCleaned; 732 } 733 }