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