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 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.loggers;
018
019import java.io.File;
020import java.io.IOException;
021import java.util.List;
022import java.util.Map;
023
024import org.forgerock.i18n.LocalizableMessage;
025import org.forgerock.opendj.config.server.ConfigChangeResult;
026import org.forgerock.opendj.config.server.ConfigException;
027import org.forgerock.opendj.config.server.ConfigurationAddListener;
028import org.forgerock.opendj.config.server.ConfigurationChangeListener;
029import org.forgerock.opendj.config.server.ConfigurationDeleteListener;
030import org.forgerock.opendj.server.config.server.DebugTargetCfg;
031import org.forgerock.opendj.server.config.server.FileBasedDebugLogPublisherCfg;
032import org.opends.server.api.DirectoryThread;
033import org.opends.server.core.DirectoryServer;
034import org.opends.server.core.ServerContext;
035import org.forgerock.opendj.ldap.DN;
036import org.opends.server.types.DirectoryException;
037import org.opends.server.types.FilePermission;
038import org.opends.server.types.InitializationException;
039import org.opends.server.util.TimeThread;
040
041import static org.opends.messages.ConfigMessages.*;
042import static org.opends.server.util.StaticUtils.*;
043
044/**
045 * The debug log publisher implementation that writes debug messages to files
046 * on disk. It also maintains the rotation and retention polices of the log
047 * files.
048 */
049public class TextDebugLogPublisher
050    extends DebugLogPublisher<FileBasedDebugLogPublisherCfg>
051    implements ConfigurationChangeListener<FileBasedDebugLogPublisherCfg>,
052               ConfigurationAddListener<DebugTargetCfg>,
053               ConfigurationDeleteListener<DebugTargetCfg>
054{
055  private static long globalSequenceNumber;
056  private TextWriter writer;
057  private FileBasedDebugLogPublisherCfg currentConfig;
058
059  /**
060   * Returns an instance of the text debug log publisher that will print all
061   * messages to the provided writer, based on the provided debug targets.
062   *
063   * @param debugTargets
064   *          The targets defining which and how debug events are logged.
065   * @param writer
066   *          The text writer where the message will be written to.
067   * @return The instance of the text error log publisher that will print all
068   *         messages to standard out. May be {@code null} if no debug target is
069   *         valid.
070   */
071  static TextDebugLogPublisher getStartupTextDebugPublisher(List<String> debugTargets, TextWriter writer)
072  {
073    TextDebugLogPublisher startupPublisher = null;
074    for (String value : debugTargets)
075    {
076      int settingsStart = value.indexOf(":");
077
078      //See if the scope and settings exists
079      if (settingsStart > 0)
080      {
081        String scope = value.substring(0, settingsStart);
082        TraceSettings settings = TraceSettings.parseTraceSettings(value.substring(settingsStart + 1));
083        if (settings != null)
084        {
085          if (startupPublisher == null) {
086            startupPublisher = new TextDebugLogPublisher();
087            startupPublisher.writer = writer;
088          }
089          startupPublisher.addTraceSettings(scope, settings);
090        }
091      }
092    }
093    return startupPublisher;
094  }
095
096  @Override
097  public boolean isConfigurationAcceptable(
098      FileBasedDebugLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
099  {
100    return isConfigurationChangeAcceptable(config, unacceptableReasons);
101  }
102
103  @Override
104  public void initializeLogPublisher(FileBasedDebugLogPublisherCfg config, ServerContext serverContext)
105      throws ConfigException, InitializationException
106  {
107    File logFile = getFileForPath(config.getLogFile(), serverContext);
108    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
109
110    try
111    {
112      FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
113      LogPublisherErrorHandler errorHandler = new LogPublisherErrorHandler(config.dn());
114      boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
115
116      MultifileTextWriter writer = new MultifileTextWriter("Multifile Text Writer for " + config.dn(),
117                                  config.getTimeInterval(),
118                                  fnPolicy,
119                                  perm,
120                                  errorHandler,
121                                  "UTF-8",
122                                  writerAutoFlush,
123                                  config.isAppend(),
124                                  (int)config.getBufferSize());
125
126      // Validate retention and rotation policies.
127      for(DN dn : config.getRotationPolicyDNs())
128      {
129        writer.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
130      }
131      for(DN dn: config.getRetentionPolicyDNs())
132      {
133        writer.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
134      }
135
136      if(config.isAsynchronous())
137      {
138        this.writer = newAsyncWriter(writer, config);
139      }
140      else
141      {
142        this.writer = writer;
143      }
144    }
145    catch(DirectoryException e)
146    {
147      throw new InitializationException(
148          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(config.dn(), e), e);
149    }
150    catch(IOException e)
151    {
152      throw new InitializationException(
153          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, config.dn(), e), e);
154    }
155
156    config.addDebugTargetAddListener(this);
157    config.addDebugTargetDeleteListener(this);
158
159    addTraceSettings(null, getDefaultSettings(config));
160
161    for(String name : config.listDebugTargets())
162    {
163      final DebugTargetCfg targetCfg = config.getDebugTarget(name);
164      addTraceSettings(targetCfg.getDebugScope(), new TraceSettings(targetCfg));
165    }
166
167    currentConfig = config;
168
169    config.addFileBasedDebugChangeListener(this);
170  }
171
172  @Override
173  public boolean isConfigurationChangeAcceptable(
174      FileBasedDebugLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
175  {
176    // Make sure the permission is valid.
177    try
178    {
179      FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
180      if (!filePerm.isOwnerWritable())
181      {
182        LocalizableMessage message = ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions());
183        unacceptableReasons.add(message);
184        return false;
185      }
186    }
187    catch (DirectoryException e)
188    {
189      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
190      return false;
191    }
192
193    return true;
194  }
195
196  @Override
197  public ConfigChangeResult applyConfigurationChange(FileBasedDebugLogPublisherCfg config)
198  {
199    final ConfigChangeResult ccr = new ConfigChangeResult();
200
201    addTraceSettings(null, getDefaultSettings(config));
202    DebugLogger.updateTracerSettings();
203
204    try
205    {
206      // Determine the writer we are using. If we were writing asynchronously,
207      // we need to modify the underlying writer.
208      TextWriter currentWriter;
209      if(writer instanceof AsynchronousTextWriter)
210      {
211        currentWriter = ((AsynchronousTextWriter)writer).getWrappedWriter();
212      }
213      else
214      {
215        currentWriter = writer;
216      }
217
218      if(currentWriter instanceof MultifileTextWriter)
219      {
220        MultifileTextWriter mfWriter = (MultifileTextWriter)writer;
221        configure(mfWriter, config);
222
223        if (config.isAsynchronous())
224        {
225          if (writer instanceof AsynchronousTextWriter)
226          {
227            if (hasAsyncConfigChanged(config))
228            {
229              // reinstantiate
230              final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
231              writer = newAsyncWriter(mfWriter, config);
232              previousWriter.shutdown(false);
233            }
234          }
235          else
236          {
237            // turn async text writer on
238            writer = newAsyncWriter(mfWriter, config);
239          }
240        }
241        else
242        {
243          if (writer instanceof AsynchronousTextWriter)
244          {
245            // asynchronous is being turned off, remove async text writers.
246            final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
247            writer = mfWriter;
248            previousWriter.shutdown(false);
249          }
250        }
251
252        if(currentConfig.isAsynchronous() && config.isAsynchronous() &&
253            currentConfig.getQueueSize() != config.getQueueSize())
254        {
255          ccr.setAdminActionRequired(true);
256        }
257
258        currentConfig = config;
259      }
260    }
261    catch(Exception e)
262    {
263      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
264      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
265          config.dn(), stackTraceToSingleLineString(e)));
266    }
267
268    return ccr;
269  }
270
271  private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter writer, FileBasedDebugLogPublisherCfg config)
272  {
273    String name = "Asynchronous Text Writer for " + config.dn();
274    return new AsynchronousTextWriter(name, config.getQueueSize(), config.isAutoFlush(), writer);
275  }
276
277  private void configure(MultifileTextWriter mfWriter, FileBasedDebugLogPublisherCfg config) throws DirectoryException
278  {
279    FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
280    boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
281
282    File logFile = getLogFile(config);
283    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
284
285    mfWriter.setNamingPolicy(fnPolicy);
286    mfWriter.setFilePermissions(perm);
287    mfWriter.setAppend(config.isAppend());
288    mfWriter.setAutoFlush(writerAutoFlush);
289    mfWriter.setBufferSize((int)config.getBufferSize());
290    mfWriter.setInterval(config.getTimeInterval());
291
292    mfWriter.removeAllRetentionPolicies();
293    mfWriter.removeAllRotationPolicies();
294    for(DN dn : config.getRotationPolicyDNs())
295    {
296      mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
297    }
298    for(DN dn: config.getRetentionPolicyDNs())
299    {
300      mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
301    }
302  }
303
304  private File getLogFile(FileBasedDebugLogPublisherCfg config)
305  {
306    return getFileForPath(config.getLogFile());
307  }
308
309  private boolean hasAsyncConfigChanged(FileBasedDebugLogPublisherCfg newConfig)
310  {
311    return !currentConfig.dn().equals(newConfig.dn())
312        && currentConfig.isAutoFlush() != newConfig.isAutoFlush()
313        && currentConfig.getQueueSize() != newConfig.getQueueSize();
314  }
315
316  private TraceSettings getDefaultSettings(FileBasedDebugLogPublisherCfg config)
317  {
318    return new TraceSettings(
319        TraceSettings.Level.getLevel(true, config.isDefaultDebugExceptionsOnly()),
320        config.isDefaultOmitMethodEntryArguments(),
321        config.isDefaultOmitMethodReturnValue(),
322        config.getDefaultThrowableStackFrames(),
323        config.isDefaultIncludeThrowableCause());
324  }
325
326  @Override
327  public boolean isConfigurationAddAcceptable(DebugTargetCfg config,
328                                              List<LocalizableMessage> unacceptableReasons)
329  {
330    return !hasTraceSettings(config.getDebugScope());
331  }
332
333  @Override
334  public boolean isConfigurationDeleteAcceptable(DebugTargetCfg config,
335                                              List<LocalizableMessage> unacceptableReasons)
336  {
337    // A delete should always be acceptable.
338    return true;
339  }
340
341  @Override
342  public ConfigChangeResult applyConfigurationAdd(DebugTargetCfg config)
343  {
344    addTraceSettings(config.getDebugScope(), new TraceSettings(config));
345
346    DebugLogger.updateTracerSettings();
347
348    return new ConfigChangeResult();
349  }
350
351  @Override
352  public ConfigChangeResult applyConfigurationDelete(DebugTargetCfg config)
353  {
354    removeTraceSettings(config.getDebugScope());
355
356    DebugLogger.updateTracerSettings();
357
358    return new ConfigChangeResult();
359  }
360
361  @Override
362  public void trace(TraceSettings settings, String signature,
363      String sourceLocation, String msg, StackTraceElement[] stackTrace)
364  {
365    String stack = null;
366    if (stackTrace != null)
367    {
368      stack = DebugStackTraceFormatter.formatStackTrace(stackTrace,
369          settings.getStackDepth());
370    }
371    publish(signature, sourceLocation, msg, stack);
372  }
373
374  @Override
375  public void traceException(TraceSettings settings, String signature,
376      String sourceLocation, String msg, Throwable ex,
377      StackTraceElement[] stackTrace)
378  {
379    String message = DebugMessageFormatter.format("%s caught={%s}", new Object[] { msg, ex });
380
381    String stack = null;
382    if (stackTrace != null)
383    {
384      stack = DebugStackTraceFormatter.formatStackTrace(ex, settings.getStackDepth(),
385          settings.isIncludeCause());
386    }
387    publish(signature, sourceLocation, message, stack);
388  }
389
390  @Override
391  public void close()
392  {
393    writer.shutdown();
394
395    if(currentConfig != null)
396    {
397      currentConfig.removeFileBasedDebugChangeListener(this);
398    }
399  }
400
401  /**
402   * Publishes a record, optionally performing some "special" work:
403   * - injecting a stack trace into the message
404   * - format the message with argument values
405   */
406  private void publish(String signature, String sourceLocation, String msg,
407                       String stack)
408  {
409    Thread thread = Thread.currentThread();
410
411    StringBuilder buf = new StringBuilder();
412    // Emit the timestamp.
413    buf.append("[");
414    buf.append(TimeThread.getLocalTime());
415    buf.append("] ");
416
417    // Emit the seq num
418    buf.append(globalSequenceNumber++);
419    buf.append(" ");
420
421    // Emit the debug level.
422    buf.append("trace ");
423
424    // Emit thread info.
425    buf.append("thread={");
426    buf.append(thread.getName());
427    buf.append("(");
428    buf.append(thread.getId());
429    buf.append(")} ");
430
431    if(thread instanceof DirectoryThread)
432    {
433      buf.append("threadDetail={");
434      for (Map.Entry<String, String> entry :
435        ((DirectoryThread) thread).getDebugProperties().entrySet())
436      {
437        buf.append(entry.getKey());
438        buf.append("=");
439        buf.append(entry.getValue());
440        buf.append(" ");
441      }
442      buf.append("} ");
443    }
444
445    // Emit method info.
446    buf.append("method={");
447    buf.append(signature);
448    buf.append("(");
449    buf.append(sourceLocation);
450    buf.append(")} ");
451
452    // Emit message.
453    buf.append(msg);
454
455    // Emit Stack Trace.
456    if(stack != null)
457    {
458      buf.append("\nStack Trace:\n");
459      buf.append(stack);
460    }
461
462    writer.writeRecord(buf.toString());
463  }
464
465  @Override
466  public DN getDN()
467  {
468    if(currentConfig != null)
469    {
470      return currentConfig.dn();
471    }
472    return null;
473  }
474}