/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2010-2024 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import java.io.File; import java.io.FileWriter; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import com.unboundid.directory.sdk.common.api.DiskSpaceConsumer; import com.unboundid.directory.sdk.common.api.ErrorLogger; import com.unboundid.directory.sdk.common.api.MonitorProvider; import com.unboundid.directory.sdk.common.config.ErrorLoggerConfig; import com.unboundid.directory.sdk.common.types.LogCategory; import com.unboundid.directory.sdk.common.types.LogSeverity; import com.unboundid.directory.sdk.common.types.RegisteredDiskSpaceConsumer; import com.unboundid.directory.sdk.common.types.RegisteredMonitorProvider; import com.unboundid.directory.sdk.common.types.ServerContext; import com.unboundid.ldap.sdk.Attribute; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.StaticUtils; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.FileArgument; /** * This class provides a simple example of an error logger that will append a * message to a specified file about any errors, warnings, or events in the * server. It takes a single configuration argument: * <UL> * <LI>log-file -- The path to the log file that will be written. This must * be provided.</LI> * </UL> * This example also demonstrates how to register an ad-hoc monitor provider * with the server, without having to add any server-side configuration. */ public final class ExampleErrorLogger extends ErrorLogger implements DiskSpaceConsumer { /** * The name of the argument that will be used for the argument used to specify * the path to the log file. */ private static final String ARG_NAME_LOG_FILE = "log-file"; // The general configuration for this error logger. private volatile ErrorLoggerConfig config; // The path to the log file to be written. private volatile File logFile; // The lock that will be used to synchronize logging activity. private final Object loggerLock; // The print writer that will be used to actually write the log messages. private volatile PrintWriter writer; // A counter used to keep track of how many messages have been logged. private AtomicLong messageCount; // The registered disk space consumer for this error logger. private volatile RegisteredDiskSpaceConsumer registeredConsumer; //The registered monitor provider for this error logger. private RegisteredMonitorProvider monitorProvider; // The server context for the server in which this extension is running. private ServerContext serverContext; /** * Creates a new instance of this error logger. All error logger * implementations must include a default constructor, but any initialization * should generally be done in the {@code initializeErrorLogger} method. */ public ExampleErrorLogger() { loggerLock = new Object(); } /** * Retrieves a human-readable name for this extension. * * @return A human-readable name for this extension. */ @Override() public String getExtensionName() { return "Example Error Logger"; } /** * Retrieves a human-readable description for this extension. Each element * of the array that is returned will be considered a separate paragraph in * generated documentation. * * @return A human-readable description for this extension, or {@code null} * or an empty array if no description should be available. */ @Override() public String[] getExtensionDescription() { return new String[] { "This error logger serves an example that may be used to demonstrate " + "the process for creating a third-party error logger. It will " + "write a line to a specified file for each error log message " + "generated by the server.", "For simplicity, this example logger does not take care of all tasks " + "that would be necessary for production use. For example, it " + "does not include any rotation or retention functionality, so the " + "file will grow without bounds. Further, while it is threadsafe, " + "it does so using simple Java synchronized blocks which is a " + "simple way to achieve the necessary safety, but does not " + "necessarily allow for the highest performance or concurrency.", "Because this error logger writes to disk, it also serves as an " + "example of a disk space consumer so that the server may track " + "available space on the disk containing the log file and " + "potentially warn administrators if usable space becomes too low." }; } /** * Updates the provided argument parser to define any configuration arguments * which may be used by this error logger. The argument parser may also * be updated to define relationships between arguments (e.g., to specify * required, exclusive, or dependent argument sets). * * @param parser The argument parser to be updated with the configuration * arguments which may be used by this error logger. * * @throws ArgumentException If a problem is encountered while updating the * provided argument parser. */ @Override() public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { // Add an argument that allows you to specify the path to the log file. Character shortIdentifier = null; String longIdentifier = ARG_NAME_LOG_FILE; boolean required = true; int maxOccurrences = 1; String placeholder = "{path}"; String description = "The path to the log file to be written. " + "Non-absolute paths will be treated as relative to the server root."; boolean fileMustExist = false; boolean parentMustExist = true; boolean mustBeFile = true; boolean mustBeDirectory = false; parser.addArgument(new FileArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description, fileMustExist, parentMustExist, mustBeFile, mustBeDirectory)); } /** * Initializes this error logger. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this error logger. * @param parser The argument parser which has been initialized from * the configuration for this error logger. * * @throws LDAPException If a problem occurs while initializing this error * logger. */ @Override() public void initializeErrorLogger(final ServerContext serverContext, final ErrorLoggerConfig config, final ArgumentParser parser) throws LDAPException { this.serverContext = serverContext; this.config = config; this.messageCount = new AtomicLong(); // Create the logger and open the log file. try { final FileArgument arg = (FileArgument) parser.getNamedArgument(ARG_NAME_LOG_FILE); logFile = arg.getValue(); writer = new PrintWriter(new FileWriter(logFile, true)); } catch (final Exception e) { serverContext.debugCaught(e); throw new LDAPException(ResultCode.OTHER, "Unable to open file " + logFile.getAbsolutePath() + " for writing: " + StaticUtils.getExceptionMessage(e), e); } // Register as a disk space consumer since we will be writing to the log // file. registeredConsumer = serverContext.registerDiskSpaceConsumer(this); // Register a custom monitor provider to expose the number of messages // logged and the size of the log file. monitorProvider = serverContext.registerMonitorProvider( new SimpleMonitorProvider(), config); } /** * Indicates whether the configuration contained in the provided argument * parser represents a valid configuration for this extension. * * @param config The general configuration for this error * logger. * @param parser The argument parser which has been initialized * with the proposed configuration. * @param unacceptableReasons A list that can be updated with reasons that * the proposed configuration is not acceptable. * * @return {@code true} if the proposed configuration is acceptable, or * {@code false} if not. */ @Override() public boolean isConfigurationAcceptable(final ErrorLoggerConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { // The argument parser will handle all of the necessary validation, so // we don't need to do anything here. return true; } /** * Attempts to apply the configuration contained in the provided argument * parser. * * @param config The general configuration for this error * logger. * @param parser The argument parser which has been * initialized with the new configuration. * @param adminActionsRequired A list that can be updated with information * about any administrative actions that may be * required before one or more of the * configuration changes will be applied. * @param messages A list that can be updated with information * about the result of applying the new * configuration. * * @return A result code that provides information about the result of * attempting to apply the configuration change. */ @Override() public ResultCode applyConfiguration(final ErrorLoggerConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { this.config = config; // Get the path to the log file from the new config. final FileArgument arg = (FileArgument) parser.getNamedArgument(ARG_NAME_LOG_FILE); final File newFile = arg.getValue(); // If the log file path hasn't changed, then we don't need to do anything. if (newFile.equals(logFile)) { return ResultCode.SUCCESS; } // Create a print writer that can be used to write to the new log file. final PrintWriter newWriter; try { newWriter = new PrintWriter(new FileWriter(newFile, true)); } catch (final Exception e) { serverContext.debugCaught(e); messages.add("Unable to open new log file " + newFile.getAbsolutePath() + " for writing: " + StaticUtils.getExceptionMessage(e)); return ResultCode.OTHER; } // Swap the new logger into place. final PrintWriter oldWriter; synchronized (loggerLock) { oldWriter = writer; writer = newWriter; logFile = newFile; } // Close the old logger. oldWriter.close(); // Re-register the monitor provider with the new config. serverContext.deregisterMonitorProvider(monitorProvider); monitorProvider = serverContext.registerMonitorProvider( new SimpleMonitorProvider(), config); return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this error logger is * to be taken out of service. */ @Override() public void finalizeErrorLogger() { synchronized (loggerLock) { if (registeredConsumer != null) { serverContext.deregisterDiskSpaceConsumer(registeredConsumer); } writer.close(); writer = null; logFile = null; } serverContext.deregisterMonitorProvider(monitorProvider); } /** * Records information about the provided message, if appropriate. * * @param category The category for the message to be logged. * @param severity The severity for the message to be logged. * @param messageID The unique identifier with which the message text is * associated. * @param message The message to be logged. */ @Override() public void logError(final LogCategory category, final LogSeverity severity, final long messageID, final String message) { final StringBuilder buffer = new StringBuilder(); buffer.append(new Date()); buffer.append(" category="); buffer.append(category.name()); buffer.append(" severity="); buffer.append(severity.name()); buffer.append(" message=\""); buffer.append(message); buffer.append('"'); synchronized (loggerLock) { if (writer == null) { // This should only happen if the logger has been shut down. return; } writer.println(buffer.toString()); } // Increment the message counter. messageCount.incrementAndGet(); } /** * Retrieves a map containing examples of configurations that may be used for * this extension. The map key should be a list of sample arguments, and the * corresponding value should be a description of the behavior that will be * exhibited by the extension when used with that configuration. * * @return A map containing examples of configurations that may be used for * this extension. It may be {@code null} or empty if there should * not be any example argument sets. */ @Override() public Map<List<String>,String> getExamplesArgumentSets() { final LinkedHashMap<List<String>,String> exampleMap = new LinkedHashMap<List<String>,String>(1); exampleMap.put( Arrays.asList(ARG_NAME_LOG_FILE + "=logs/example-error.log"), "Write error log messages to the logs/example-error.log file below " + "the server root."); return exampleMap; } /** * Retrieves the name that should be used to identify this disk space * consumer. * * @return The name that should be used to identify this disk space consumer. */ public String getDiskSpaceConsumerName() { return "Example Error Logger " + config.getConfigObjectName(); } /** * Retrieves a list of filesystem paths in which this disk space consumer may * store files which may consume a significant amount of space. It is * generally recommended that the paths be directories, but they may also be * individual files. * * @return A list of filesystem paths in which this disk space consumer may * store files which may consume a significant amount of space. */ public List<File> getDiskSpaceConsumerPaths() { return Arrays.asList(logFile); } /** * A simple implementation of MonitorProvider that we can register with the * server through the ServerContext object. This exposes the number of * messages that have been logged and the current size of the log file. */ private class SimpleMonitorProvider extends MonitorProvider { /** * Retrieves a human-readable name for this extension. This is not used * since we are registering this MonitorProvider through the ServerContext * object (e.g. it is not a stand-alone extension). * * @return A human-readable name for this extension. */ @Override public String getExtensionName() { return "SimpleMonitorProvider"; } /** * Retrieves a human-readable description for this extension. Each element * of the array that is returned will be considered a separate paragraph in * generated documentation. This is not used since we are registering this * MonitorProvider through the ServerContext object (e.g. it is not a * stand-alone extension). * * @return A human-readable description for this extension, or {@code null} * or an empty array if no description should be available. */ @Override public String[] getExtensionDescription() { return null; } /** * Retrieves the name that identifies this monitor provider instance. It * will be used as the value of the naming attribute for monitor entries. * Each monitor provider instance must have a unique name. * * @return The name that identifies this monitor provider instance. */ @Override public String getMonitorInstanceName() { return "ExampleErrorLogger Monitor"; } /** * Retrieves the name of the object class that will be used for monitor * entries created from this monitor provider. This does not need to be * defined in the server schema. It may be {@code null} if a default object * class should be used. * * @return The name of the object class that will be used for monitor * entries created from this monitor provider. */ @Override public String getMonitorObjectClass() { return "error-logger-monitor-entry"; } /** * Retrieves a list of attributes containing the data to include in the * monitor entry generated from this monitor provider. * * @return A list of attributes containing the data to include in the * monitor entry generated from this monitor provider. */ @Override public List<Attribute> getMonitorAttributes() { List<Attribute> attributes = new ArrayList<Attribute>(2); attributes.add(new Attribute("num-messages-logged", String.valueOf(messageCount.get()))); attributes.add(new Attribute("log-file-size", logFile.getTotalSpace() + " bytes")); return attributes; } } }