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 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.loggers;
018
019import static org.opends.messages.ConfigMessages.*;
020import static org.opends.messages.LoggerMessages.*;
021import static org.opends.server.util.ServerConstants.*;
022import static org.opends.server.util.StaticUtils.*;
023
024import java.io.File;
025import java.io.IOException;
026import java.util.Arrays;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030import java.util.StringTokenizer;
031
032import org.forgerock.i18n.LocalizableMessage;
033import org.forgerock.opendj.config.server.ConfigChangeResult;
034import org.forgerock.opendj.config.server.ConfigException;
035import org.opends.messages.Severity;
036import org.forgerock.opendj.config.server.ConfigurationChangeListener;
037import org.forgerock.opendj.server.config.meta.ErrorLogPublisherCfgDefn;
038import org.forgerock.opendj.server.config.server.FileBasedErrorLogPublisherCfg;
039import org.opends.server.core.DirectoryServer;
040import org.opends.server.core.ServerContext;
041import org.forgerock.opendj.ldap.DN;
042import org.opends.server.types.DirectoryException;
043import org.opends.server.types.FilePermission;
044import org.opends.server.types.InitializationException;
045import org.opends.server.util.StaticUtils;
046import org.opends.server.util.TimeThread;
047
048/** This class provides an implementation of an error log publisher. */
049public class TextErrorLogPublisher
050    extends ErrorLogPublisher<FileBasedErrorLogPublisherCfg>
051    implements ConfigurationChangeListener<FileBasedErrorLogPublisherCfg>
052{
053  private TextWriter writer;
054  private FileBasedErrorLogPublisherCfg currentConfig;
055  private ServerContext serverContext;
056
057  /**
058   * Returns a new text error log publisher which will print all messages to the
059   * provided writer. This publisher should be used by tools.
060   *
061   * @param writer
062   *          The text writer where the message will be written to.
063   * @return A new text error log publisher which will print all messages to the
064   *         provided writer.
065   */
066  public static TextErrorLogPublisher getToolStartupTextErrorPublisher(TextWriter writer)
067  {
068    TextErrorLogPublisher startupPublisher = new TextErrorLogPublisher();
069    startupPublisher.writer = writer;
070    startupPublisher.defaultSeverities.addAll(Arrays.asList(Severity.values()));
071    return startupPublisher;
072  }
073
074  /**
075   * Returns a new text error log publisher which will print only notices,
076   * severe warnings and errors, and fatal errors messages to the provided
077   * writer. This less verbose publisher should be used by the directory server
078   * during startup.
079   *
080   * @param writer
081   *          The text writer where the message will be written to.
082   * @return A new text error log publisher which will print only notices,
083   *         severe warnings and errors, and fatal errors messages to the
084   *         provided writer.
085   */
086  public static TextErrorLogPublisher getServerStartupTextErrorPublisher(TextWriter writer)
087  {
088    TextErrorLogPublisher startupPublisher = new TextErrorLogPublisher();
089    startupPublisher.writer = writer;
090    startupPublisher.defaultSeverities.addAll(Arrays.asList(
091        Severity.ERROR, Severity.WARNING, Severity.NOTICE));
092    return startupPublisher;
093  }
094
095  @Override
096  public void initializeLogPublisher(FileBasedErrorLogPublisherCfg config, ServerContext serverContext)
097      throws ConfigException, InitializationException
098  {
099    this.serverContext = serverContext;
100    File logFile = getLogFile(config);
101    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
102
103    try
104    {
105      FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
106      LogPublisherErrorHandler errorHandler = new LogPublisherErrorHandler(config.dn());
107      boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
108
109      MultifileTextWriter writer = new MultifileTextWriter("Multifile Text Writer for " + config.dn(),
110                                  config.getTimeInterval(),
111                                  fnPolicy,
112                                  perm,
113                                  errorHandler,
114                                  "UTF-8",
115                                  writerAutoFlush,
116                                  config.isAppend(),
117                                  (int)config.getBufferSize());
118
119      // Validate retention and rotation policies.
120      for(DN dn : config.getRotationPolicyDNs())
121      {
122        writer.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
123      }
124      for(DN dn: config.getRetentionPolicyDNs())
125      {
126        writer.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
127      }
128
129      if(config.isAsynchronous())
130      {
131        this.writer = newAsyncWriter(writer, config);
132      }
133      else
134      {
135        this.writer = writer;
136      }
137    }
138    catch(DirectoryException e)
139    {
140      throw new InitializationException(
141          ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(config.dn(), e), e);
142    }
143    catch(IOException e)
144    {
145      throw new InitializationException(
146          ERR_CONFIG_LOGGING_CANNOT_OPEN_FILE.get(logFile, config.dn(), e), e);
147    }
148
149    setDefaultSeverities(config.getDefaultSeverity());
150
151    ConfigChangeResult ccr = new ConfigChangeResult();
152    setDefinedSeverities(config, ccr);
153    if (!ccr.getMessages().isEmpty())
154    {
155      throw new ConfigException(ccr.getMessages().iterator().next());
156    }
157
158    currentConfig = config;
159
160    config.addFileBasedErrorChangeListener(this);
161  }
162
163  private void setDefinedSeverities(FileBasedErrorLogPublisherCfg config, final ConfigChangeResult ccr)
164  {
165    for (String overrideSeverity : config.getOverrideSeverity())
166    {
167      if (overrideSeverity != null)
168      {
169        int equalPos = overrideSeverity.indexOf('=');
170        if (equalPos < 0)
171        {
172          ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
173          ccr.addMessage(WARN_ERROR_LOGGER_INVALID_OVERRIDE_SEVERITY.get(overrideSeverity));
174          return;
175        }
176
177        String category = overrideSeverity.substring(0, equalPos);
178        category = category.replace("-", "_").toUpperCase();
179        try
180        {
181          Set<Severity> severities = new HashSet<>();
182          StringTokenizer sevTokenizer = new StringTokenizer(overrideSeverity.substring(equalPos + 1), ",");
183          while (sevTokenizer.hasMoreElements())
184          {
185            String severityName = sevTokenizer.nextToken();
186            severityName = severityName.replace("-", "_").toUpperCase();
187            if (LOG_SEVERITY_ALL.equalsIgnoreCase(severityName))
188            {
189              addAllSeverities(severities);
190            }
191            else
192            {
193              try
194              {
195                severities.add(Severity.parseString(severityName));
196              }
197              catch (Exception e)
198              {
199                ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
200                ccr.addMessage(WARN_ERROR_LOGGER_INVALID_SEVERITY.get(severityName));
201                return;
202              }
203            }
204          }
205          definedSeverities.put(category, severities);
206        }
207        catch (Exception e)
208        {
209          ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
210          ccr.addMessage(WARN_ERROR_LOGGER_INVALID_CATEGORY.get(category));
211          return;
212        }
213      }
214    }
215  }
216
217  @Override
218  public boolean isConfigurationAcceptable(
219      FileBasedErrorLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
220  {
221    return isConfigurationChangeAcceptable(config, unacceptableReasons);
222  }
223
224  @Override
225  public boolean isConfigurationChangeAcceptable(
226      FileBasedErrorLogPublisherCfg config, List<LocalizableMessage> unacceptableReasons)
227  {
228    // Make sure the permission is valid.
229    try
230    {
231      FilePermission filePerm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
232      if(!filePerm.isOwnerWritable())
233      {
234        unacceptableReasons.add(ERR_CONFIG_LOGGING_INSANE_MODE.get(config.getLogFilePermissions()));
235        return false;
236      }
237    }
238    catch(DirectoryException e)
239    {
240      unacceptableReasons.add(ERR_CONFIG_LOGGING_MODE_INVALID.get(config.getLogFilePermissions(), e));
241      return false;
242    }
243
244    for(String overrideSeverity : config.getOverrideSeverity())
245    {
246      if(overrideSeverity != null)
247      {
248        int equalPos = overrideSeverity.indexOf('=');
249        if (equalPos < 0)
250        {
251          unacceptableReasons.add(WARN_ERROR_LOGGER_INVALID_OVERRIDE_SEVERITY.get(overrideSeverity));
252          return false;
253        }
254
255        // No check on category because it can be any value
256        StringTokenizer sevTokenizer = new StringTokenizer(overrideSeverity.substring(equalPos + 1), ",");
257        while (sevTokenizer.hasMoreElements())
258        {
259          String severityName = sevTokenizer.nextToken();
260          severityName = severityName.replace("-", "_").toUpperCase();
261          if (!LOG_SEVERITY_ALL.equalsIgnoreCase(severityName))
262          {
263            try
264            {
265              Severity.parseString(severityName);
266            }
267            catch (Exception e)
268            {
269              unacceptableReasons.add(WARN_ERROR_LOGGER_INVALID_SEVERITY.get(severityName));
270              return false;
271            }
272          }
273        }
274      }
275    }
276    return true;
277  }
278
279  @Override
280  public ConfigChangeResult applyConfigurationChange(FileBasedErrorLogPublisherCfg config)
281  {
282    final ConfigChangeResult ccr = new ConfigChangeResult();
283
284    setDefaultSeverities(config.getDefaultSeverity());
285
286    definedSeverities.clear();
287    setDefinedSeverities(config, ccr);
288
289    try
290    {
291      // Determine the writer we are using. If we were writing asynchronously,
292      // we need to modify the underlying writer.
293      TextWriter currentWriter;
294      if(writer instanceof AsynchronousTextWriter)
295      {
296        currentWriter = ((AsynchronousTextWriter)writer).getWrappedWriter();
297      }
298      else
299      {
300        currentWriter = writer;
301      }
302
303      if(currentWriter instanceof MultifileTextWriter)
304      {
305        MultifileTextWriter mfWriter = (MultifileTextWriter)currentWriter;
306        configure(mfWriter, config);
307
308        if (config.isAsynchronous())
309        {
310          if (writer instanceof AsynchronousTextWriter)
311          {
312            if (hasAsyncConfigChanged(config))
313            {
314              // reinstantiate
315              final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
316              writer = newAsyncWriter(mfWriter, config);
317              previousWriter.shutdown(false);
318            }
319          }
320          else
321          {
322            // turn async text writer on
323            writer = newAsyncWriter(mfWriter, config);
324          }
325        }
326        else
327        {
328          if (writer instanceof AsynchronousTextWriter)
329          {
330            // asynchronous is being turned off, remove async text writers.
331            final AsynchronousTextWriter previousWriter = (AsynchronousTextWriter) writer;
332            writer = mfWriter;
333            previousWriter.shutdown(false);
334          }
335        }
336
337        if (currentConfig.isAsynchronous() && config.isAsynchronous()
338            && currentConfig.getQueueSize() != config.getQueueSize())
339        {
340          ccr.setAdminActionRequired(true);
341        }
342
343        currentConfig = config;
344      }
345      serverContext.getLoggerConfigManager().adjustJulLevel();
346    }
347    catch(Exception e)
348    {
349      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
350      ccr.addMessage(ERR_CONFIG_LOGGING_CANNOT_CREATE_WRITER.get(
351          config.dn(), stackTraceToSingleLineString(e)));
352    }
353
354    return ccr;
355  }
356
357  private void configure(MultifileTextWriter mfWriter, FileBasedErrorLogPublisherCfg config) throws DirectoryException
358  {
359    FilePermission perm = FilePermission.decodeUNIXMode(config.getLogFilePermissions());
360    boolean writerAutoFlush = config.isAutoFlush() && !config.isAsynchronous();
361
362    File logFile = getLogFile(config);
363    FileNamingPolicy fnPolicy = new TimeStampNaming(logFile);
364
365    mfWriter.setNamingPolicy(fnPolicy);
366    mfWriter.setFilePermissions(perm);
367    mfWriter.setAppend(config.isAppend());
368    mfWriter.setAutoFlush(writerAutoFlush);
369    mfWriter.setBufferSize((int) config.getBufferSize());
370    mfWriter.setInterval(config.getTimeInterval());
371
372    mfWriter.removeAllRetentionPolicies();
373    mfWriter.removeAllRotationPolicies();
374    for (DN dn : config.getRotationPolicyDNs())
375    {
376      mfWriter.addRotationPolicy(DirectoryServer.getRotationPolicy(dn));
377    }
378    for (DN dn : config.getRetentionPolicyDNs())
379    {
380      mfWriter.addRetentionPolicy(DirectoryServer.getRetentionPolicy(dn));
381    }
382  }
383
384  private File getLogFile(FileBasedErrorLogPublisherCfg config)
385  {
386    return getFileForPath(config.getLogFile());
387  }
388
389  private boolean hasAsyncConfigChanged(FileBasedErrorLogPublisherCfg newConfig)
390  {
391    return !currentConfig.dn().equals(newConfig.dn())
392        && currentConfig.isAutoFlush() != newConfig.isAutoFlush()
393        && currentConfig.getQueueSize() != newConfig.getQueueSize();
394  }
395
396  private AsynchronousTextWriter newAsyncWriter(MultifileTextWriter mfWriter, FileBasedErrorLogPublisherCfg config)
397  {
398    String name = "Asynchronous Text Writer for " + config.dn();
399    return new AsynchronousTextWriter(name, config.getQueueSize(), config.isAutoFlush(), mfWriter);
400  }
401
402  private void setDefaultSeverities(Set<ErrorLogPublisherCfgDefn.DefaultSeverity> defSevs)
403  {
404    defaultSeverities.clear();
405    if (defSevs.isEmpty())
406    {
407      defaultSeverities.add(Severity.ERROR);
408      defaultSeverities.add(Severity.WARNING);
409    }
410    else
411    {
412      for (ErrorLogPublisherCfgDefn.DefaultSeverity defSev : defSevs)
413      {
414        String defaultSeverity = defSev.toString();
415        if (LOG_SEVERITY_ALL.equalsIgnoreCase(defaultSeverity))
416        {
417          addAllSeverities(defaultSeverities);
418        }
419        else if (!LOG_SEVERITY_NONE.equalsIgnoreCase(defaultSeverity))
420        {
421          Severity errorSeverity = Severity.parseString(defSev.name());
422          if (errorSeverity != null)
423          {
424            defaultSeverities.add(errorSeverity);
425          }
426        }
427      }
428    }
429  }
430
431  private void addAllSeverities(Set<Severity> severities)
432  {
433    severities.add(Severity.ERROR);
434    severities.add(Severity.WARNING);
435    severities.add(Severity.INFORMATION);
436    severities.add(Severity.NOTICE);
437  }
438
439  @Override
440  public void close()
441  {
442    writer.shutdown();
443
444    if(currentConfig != null)
445    {
446      currentConfig.removeFileBasedErrorChangeListener(this);
447    }
448  }
449
450  @Override
451  public void log(String source, Severity severity, LocalizableMessage message, Throwable exception)
452  {
453    String category = LoggingCategoryNames.getCategoryName(message.resourceName(), source);
454    if (isEnabledFor(category, severity))
455    {
456      StringBuilder sb = new StringBuilder()
457          .append("[")
458          .append(TimeThread.getLocalTime())
459          .append("] category=")
460          .append(category)
461          .append(" severity=")
462          .append(severity)
463          .append(" msgID=")
464          .append(message.ordinal())
465          .append(" msg=")
466          .append(message.toString());
467      if (exception != null)
468      {
469        sb.append(" exception=").append(StaticUtils.stackTraceToSingleLineString(exception));
470      }
471
472      writer.writeRecord(sb.toString());
473    }
474  }
475
476  @Override
477  public boolean isEnabledFor(String category, Severity severity)
478  {
479    Set<Severity> severities = definedSeverities.get(category);
480    if (severities == null)
481    {
482      severities = defaultSeverities;
483    }
484    return severities.contains(severity);
485  }
486
487  @Override
488  public DN getDN()
489  {
490    if(currentConfig != null)
491    {
492      return currentConfig.dn();
493    }
494    return null;
495  }
496}