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