mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Yuriy Movchan
30.08.2021 2cf46088b7e69b4f424a821291607afe6faa7e4f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 */
package org.opends.server.loggers;
 
 
import static org.opends.messages.LoggerMessages.*;
import static org.opends.server.util.StaticUtils.*;
 
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
 
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.server.config.server.SizeLimitLogRotationPolicyCfg;
import org.opends.server.api.DirectoryThread;
import org.opends.server.api.ServerShutdownListener;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.FilePermission;
import org.opends.server.util.TimeThread;
 
/**
 * A MultiFileTextWriter is a specialized TextWriter which supports publishing
 * log records to a set of files. MultiFileWriters write to one file in the
 * set at a time, switching files as is dictated by a specified rotation
 * and retention policies.
 *
 * When a switch is required, the writer closes the current file and opens a
 * new one named in accordance with a specified FileNamingPolicy.
 */
class MultifileTextWriter
    implements ServerShutdownListener, TextWriter, RotatableLogFile,
    ConfigurationChangeListener<SizeLimitLogRotationPolicyCfg>
{
 
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
 
  private static final String UTF8_ENCODING= "UTF-8";
 
  private final CopyOnWriteArrayList<RotationPolicy<?>> rotationPolicies = new CopyOnWriteArrayList<>();
  private final CopyOnWriteArrayList<RetentionPolicy<?>> retentionPolicies = new CopyOnWriteArrayList<>();
 
  private FileNamingPolicy namingPolicy;
  private FilePermission filePermissions;
  private final LogPublisherErrorHandler errorHandler;
 
  private final String name;
  private final String encoding;
  private int bufferSize;
  private boolean autoFlush;
  private boolean append;
  private long interval;
  private boolean stopRequested;
  private long sizeLimit;
 
  private final Thread rotaterThread;
 
  private Calendar lastRotationTime = TimeThread.getCalendar();
  private Calendar lastCleanTime = TimeThread.getCalendar();
  private long lastCleanCount;
  private long totalFilesRotated;
  private long totalFilesCleaned;
 
  /** The underlying output stream. */
  private MeteredStream outputStream;
  /** The underlying buffered writer using the output stream. */
  private BufferedWriter writer;
 
  /**
   * Creates a new instance of MultiFileTextWriter with the supplied policies.
   *
   * @param name the name of the log rotation thread.
   * @param interval the interval to check whether the logs need to be rotated.
   * @param namingPolicy the file naming policy to use to name rotated log.
   *                      files.
   * @param filePermissions the file permissions to set on the log files.
   * @param errorHandler the log publisher error handler to notify when
   *                     an error occurs.
   * @param encoding the encoding to use to write the log files.
   * @param autoFlush whether to flush the writer on every println.
   * @param append whether to append to an existing log file.
   * @param bufferSize the bufferSize to use for the writer.
   * @throws IOException if an error occurs while creating the log file.
   * @throws DirectoryException if an error occurs while preping the new log
   *                            file.
   */
  public MultifileTextWriter(String name, long interval,
                             FileNamingPolicy namingPolicy,
                             FilePermission filePermissions,
                             LogPublisherErrorHandler errorHandler,
                             String encoding,
                             boolean autoFlush,
                             boolean append,
                             int bufferSize)
      throws IOException, DirectoryException
  {
    File file = namingPolicy.getInitialName();
    constructWriter(file, filePermissions, encoding, append,
                    bufferSize);
 
    this.name = name;
    this.interval = interval;
    this.namingPolicy = namingPolicy;
    this.filePermissions = filePermissions;
    this.errorHandler = errorHandler;
 
    this.encoding = UTF8_ENCODING;
    this.autoFlush = autoFlush;
    this.append = append;
    this.bufferSize = bufferSize;
 
    this.stopRequested = false;
 
    rotaterThread = new RotaterThread(this);
    rotaterThread.start();
 
    DirectoryServer.registerShutdownListener(this);
  }
 
  /**
   * Construct a PrintWriter for a file.
   * @param file - the file to open for writing
   * @param filePermissions - the file permissions to set on the file.
   * @param encoding - the encoding to use when writing log records.
   * @param append - indicates whether the file should be appended to or
   * truncated.
   * @param bufferSize - the buffer size to use for the writer.
   * @throws IOException if the PrintWriter could not be constructed
   * or if the file already exists and it was indicated this should be
   * an error.
   * @throws DirectoryException if there was a problem setting permissions on
   * the file.
   */
  private void constructWriter(File file, FilePermission filePermissions,
                               String encoding, boolean append,
                               int bufferSize)
      throws IOException, DirectoryException
  {
    // Create new file if it doesn't exist
    if(!file.exists())
    {
      file.createNewFile();
    }
 
    FileOutputStream stream = new FileOutputStream(file, append);
    outputStream = new MeteredStream(stream, file.length());
 
    OutputStreamWriter osw = new OutputStreamWriter(outputStream, encoding);
    if(bufferSize <= 0)
    {
      writer = new BufferedWriter(osw);
    }
    else
    {
      writer = new BufferedWriter(osw, bufferSize);
    }
 
 
    // Try to apply file permissions.
    try
    {
      if(!FilePermission.setPermissions(file, filePermissions))
      {
        logger.warn(WARN_LOGGER_UNABLE_SET_PERMISSIONS, filePermissions, file);
      }
    }
    catch(Exception e)
    {
      // Log an warning that the permissions were not set.
      logger.warn(WARN_LOGGER_SET_PERMISSION_FAILED, file, stackTraceToSingleLineString(e));
    }
  }
 
 
  /**
   * Add a rotation policy to enforce on the files written by this writer.
   *
   * @param policy The rotation policy to add.
   */
  public void addRotationPolicy(RotationPolicy<?> policy)
  {
    this.rotationPolicies.add(policy);
 
    if(policy instanceof SizeBasedRotationPolicy)
    {
      SizeBasedRotationPolicy sizePolicy = (SizeBasedRotationPolicy) policy;
      if(sizeLimit == 0 ||
          sizeLimit > sizePolicy.currentConfig.getFileSizeLimit())
      {
        sizeLimit = sizePolicy.currentConfig.getFileSizeLimit();
      }
      // Add this as a change listener so we can update the size limit.
      sizePolicy.currentConfig.addSizeLimitChangeListener(this);
    }
  }
 
  /**
   * Add a retention policy to enforce on the files written by this writer.
   *
   * @param policy The retention policy to add.
   */
  public void addRetentionPolicy(RetentionPolicy<?> policy)
  {
    this.retentionPolicies.add(policy);
  }
 
  /** Removes all the rotation policies currently enforced by this writer. */
  public void removeAllRotationPolicies()
  {
    for (RotationPolicy<?> policy : rotationPolicies)
    {
      if(policy instanceof SizeBasedRotationPolicy)
      {
        sizeLimit = 0;
 
        // Remove this as a change listener.
        SizeBasedRotationPolicy sizePolicy = (SizeBasedRotationPolicy) policy;
        sizePolicy.currentConfig.removeSizeLimitChangeListener(this);
      }
    }
 
    this.rotationPolicies.clear();
  }
 
  /** Removes all retention policies being enforced by this writer. */
  public void removeAllRetentionPolicies()
  {
    this.retentionPolicies.clear();
  }
 
  /**
   * Set the auto flush setting for this writer.
   *
   * @param autoFlush If the writer should flush the buffer after every line.
   */
  public void setAutoFlush(boolean autoFlush)
  {
    this.autoFlush = autoFlush;
  }
 
  /**
   * Set the append setting for this writter.
   *
   * @param append If the writer should append to an existing file.
   */
  public void setAppend(boolean append)
  {
    this.append = append;
  }
 
  /**
   * Set the buffer size for this writter.
   *
   * @param bufferSize The size of the underlying output stream buffer.
   */
  public void setBufferSize(int bufferSize)
  {
    this.bufferSize = bufferSize;
  }
 
  /**
   * Set the file permission to set for newly created log files.
   *
   * @param filePermissions The file permission to set for new log files.
   */
  public void setFilePermissions(FilePermission filePermissions)
  {
    this.filePermissions = filePermissions;
  }
 
  /**
   * Retrieves the current naming policy used to generate log file names.
   *
   * @return The current naming policy in use.
   */
  public FileNamingPolicy getNamingPolicy()
  {
    return namingPolicy;
  }
 
  /**
   * Set the naming policy to use when generating new log files.
   *
   * @param namingPolicy the naming policy to use to name log files.
   */
  public void setNamingPolicy(FileNamingPolicy namingPolicy)
  {
    this.namingPolicy = namingPolicy;
  }
 
  /**
   * Set the interval in which the rotator thread checks to see if the log
   * file should be rotated.
   *
   * @param interval The interval to check if the log file needs to be rotated.
   */
  public void setInterval(long interval)
  {
    this.interval = interval;
 
    // Wake up the thread if its sleeping on the old interval
    if(rotaterThread.getState() == Thread.State.TIMED_WAITING)
    {
      rotaterThread.interrupt();
    }
  }
 
  @Override
  public boolean isConfigurationChangeAcceptable(
      SizeLimitLogRotationPolicyCfg config, List<LocalizableMessage> unacceptableReasons)
  {
    // This should always be ok
    return true;
  }
 
  @Override
  public ConfigChangeResult applyConfigurationChange(
      SizeLimitLogRotationPolicyCfg config)
  {
    long newSizeLimit = Integer.MAX_VALUE;
 
    // Go through all current size rotation policies and get the lowest size setting.
    for (RotationPolicy<?> policy : rotationPolicies)
    {
      if(policy instanceof SizeBasedRotationPolicy)
      {
        SizeBasedRotationPolicy sizePolicy = (SizeBasedRotationPolicy) policy;
        SizeLimitLogRotationPolicyCfg cfg =
            sizePolicy.currentConfig.dn().equals(config.dn()) ? config : sizePolicy.currentConfig;
        if(newSizeLimit > cfg.getFileSizeLimit())
        {
          newSizeLimit = cfg.getFileSizeLimit();
        }
      }
    }
 
    sizeLimit = newSizeLimit;
 
    return new ConfigChangeResult();
  }
 
  /**
   * A rotater thread is responsible for checking if the log files need to be
   * rotated based on the policies. It will do so if necessary.
   */
  private class RotaterThread extends DirectoryThread
  {
    MultifileTextWriter writer;
 
    public RotaterThread(MultifileTextWriter writer)
    {
      super(name);
      this.writer = writer;
    }
 
    /**
     * The run method of the rotaterThread. It wakes up periodically and checks
     * whether the file needs to be rotated based on the rotation policy.
     */
    @Override
    public void run()
    {
      while(!isShuttingDown())
      {
        try
        {
          sleep(interval);
        }
        catch(InterruptedException e)
        {
          // We expect this to happen.
        }
        catch(Exception e)
        {
          logger.traceException(e);
        }
 
        for (RotationPolicy<?> rotationPolicy : rotationPolicies)
        {
          if(rotationPolicy.rotateFile(writer))
          {
            rotate();
          }
        }
 
        for (RetentionPolicy<?> retentionPolicy : retentionPolicies)
        {
          try
          {
            File[] files =
                retentionPolicy.deleteFiles(writer.getNamingPolicy());
 
            for(File file : files)
            {
              file.delete();
              totalFilesCleaned++;
              logger.trace("%s cleaned up log file %s", retentionPolicy, file);
            }
 
            if(files.length > 0)
            {
              lastCleanTime = TimeThread.getCalendar();
              lastCleanCount = files.length;
            }
          }
          catch(DirectoryException de)
          {
            logger.traceException(de);
            errorHandler.handleDeleteError(retentionPolicy, de);
          }
        }
      }
    }
  }
 
  /**
   * Retrieves the human-readable name for this shutdown listener.
   *
   * @return  The human-readable name for this shutdown listener.
   */
  @Override
  public String getShutdownListenerName()
  {
    return "MultifileTextWriter Thread " + name;
  }
 
  /**
   * Indicates that the Directory Server has received a request to stop running
   * and that this shutdown listener should take any action necessary to prepare
   * for it.
   *
   * @param  reason  The human-readable reason for the shutdown.
   */
  @Override
  public void processServerShutdown(LocalizableMessage reason)
  {
    stopRequested = true;
 
    // Wait for rotater to terminate
    while (rotaterThread != null && rotaterThread.isAlive()) {
      try {
        // Interrupt if its sleeping
        rotaterThread.interrupt();
        rotaterThread.join();
      }
      catch (InterruptedException ex) {
        // Ignore; we gotta wait..
      }
    }
 
    DirectoryServer.deregisterShutdownListener(this);
 
    removeAllRotationPolicies();
    removeAllRetentionPolicies();
 
    // Don't close the writer as there might still be message to be
    // written. manually shutdown just before the server process
    // exists.
  }
 
  /**
   * Queries whether the publisher is in shutdown mode.
   *
   * @return if the publish is in shutdown mode.
   */
  private boolean isShuttingDown()
  {
    return stopRequested;
  }
 
  /** Shutdown the text writer. */
  @Override
  public void shutdown()
  {
    processServerShutdown(null);
 
    try
    {
      writer.flush();
      writer.close();
    }
    catch(Exception e)
    {
      errorHandler.handleCloseError(e);
    }
  }
 
 
  /**
   * Write a log record string to the file.
   *
   * @param record the log record to write.
   */
  @Override
  public void writeRecord(String record)
  {
    // Assume each character is 1 byte ASCII
    int length = record.length();
    int size = length;
    char c;
    for (int i=0; i < length; i++)
    {
      c = record.charAt(i);
      if (c != (byte) (c & 0x0000007F))
      {
        try
        {
          // String contains a non ASCII character. Fall back to getBytes.
          size = record.getBytes("UTF-8").length;
        }
        catch(Exception e)
        {
          size = length * 2;
        }
        break;
      }
    }
 
    synchronized(this)
    {
      if(sizeLimit > 0 && outputStream.written + size + 1 >= sizeLimit)
      {
        rotate();
      }
 
      try
      {
        writer.write(record);
        writer.newLine();
      }
      catch(Exception e)
      {
        errorHandler.handleWriteError(record, e);
      }
 
      if(autoFlush)
      {
        flush();
      }
    }
  }
 
  @Override
  public void flush()
  {
    try
    {
      writer.flush();
    }
    catch(Exception e)
    {
      errorHandler.handleFlushError(e);
    }
  }
 
  /**
   * Tries to rotate the log files. If the new log file already exists, it
   * tries to rename the file. On failure, all subsequent log write requests
   * will throw exceptions.
   */
  private synchronized void rotate()
  {
    try
    {
      writer.flush();
      writer.close();
    }
    catch(Exception e)
    {
      logger.traceException(e);
      errorHandler.handleCloseError(e);
    }
 
    File currentFile = namingPolicy.getInitialName();
    File newFile = namingPolicy.getNextName();
    currentFile.renameTo(newFile);
 
    try
    {
      constructWriter(currentFile, filePermissions, encoding, append,
                      bufferSize);
    }
    catch (Exception e)
    {
      logger.traceException(e);
      errorHandler.handleOpenError(currentFile, e);
    }
 
    logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
    totalFilesRotated++;
    lastRotationTime = TimeThread.getCalendar();
  }
 
  @Override
  public long getBytesWritten()
  {
    return outputStream.written;
  }
 
  /**
   * Retrieves the last time one or more log files are cleaned in this instance
   * of the Directory Server. If log files have never been cleaned, this value
   * will be the time the server started.
   *
   * @return The last time log files are cleaned.
   */
  public Calendar getLastCleanTime()
  {
    return lastCleanTime;
  }
 
  /**
   * Retrieves the number of files cleaned in the last cleanup run.
   *
   * @return The number of files cleaned int he last cleanup run.
   */
  public long getLastCleanCount()
  {
    return lastCleanCount;
  }
 
  @Override
  public Calendar getLastRotationTime()
  {
    return lastRotationTime;
  }
 
  /**
   * Retrieves the total number file rotations occurred in this instance of the
   * Directory Server.
   *
   * @return The total number of file rotations.
   */
  public long getTotalFilesRotated()
  {
    return totalFilesRotated;
  }
 
  /**
   * Retrieves the total number of files cleaned in this instance of the
   * Directory Server.
   *
   * @return The total number of files cleaned.
   */
  public long getTotalFilesCleaned()
  {
    return totalFilesCleaned;
  }
}