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    }