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