All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldap.sdk.unboundidds.examples.SummarizeAccessLog Maven / Gradle / Ivy

/*
 * Copyright 2009-2023 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2009-2023 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2009-2023 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldap.sdk.unboundidds.examples;



import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.GZIPInputStream;
import javax.crypto.BadPaddingException;

import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.Version;
import com.unboundid.ldap.sdk.unboundidds.logs.LogException;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            AbandonRequestAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.AccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.AccessLogReader;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.AddResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.BindResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.CompareResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.ConnectAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.DeleteResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.DisconnectAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            ExtendedRequestAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            ExtendedResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            ModifyDNResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.ModifyResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            OperationRequestAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            OperationResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.SearchRequestAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.SearchResultAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.
            SecurityNegotiationAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.UnbindRequestAccessLogMessage;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.json.JSONAccessLogReader;
import com.unboundid.ldap.sdk.unboundidds.logs.v2.text.
            TextFormattedAccessLogReader;
import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils;
import com.unboundid.util.CommandLineTool;
import com.unboundid.util.Debug;
import com.unboundid.util.NotMutable;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.OIDRegistry;
import com.unboundid.util.OIDRegistryItem;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.ReverseComparator;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.DurationArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.IntegerArgument;



/**
 * This class provides a tool that may be used to read and summarize the
 * contents of one or more access log files from Ping Identity, UnboundID and
 * Nokia/Alcatel-Lucent 8661 server products.
 * 
*
* NOTE: This class, and other classes within the * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only * supported for use against Ping Identity, UnboundID, and * Nokia/Alcatel-Lucent 8661 server products. These classes provide support * for proprietary functionality or for external specifications that are not * considered stable or mature enough to be guaranteed to work in an * interoperable way with other types of LDAP servers. *
* Information that will be reported includes: *
    *
  • The total length of time covered by the log files.
  • *
  • The number of connections established and disconnected, the addresses * of the most commonly-connecting clients, and the average rate of * connects and disconnects per second.
  • *
  • The number of operations processed, overall and by operation type, * and the average rate of operations per second.
  • *
  • The average duration for operations processed, overall and by operation * type.
  • *
  • A breakdown of operation processing times into a number of predefined * categories, ranging from less than one millisecond to over one * minute.
  • *
  • A breakdown of the most common result codes for each type of operation * and their relative frequencies.
  • *
  • The most common types of extended requests processed and their * relative frequencies.
  • *
  • The number of unindexed search operations processed and the most common * types of filters used in unindexed searches.
  • *
  • A breakdown of the relative frequencies for each type of search * scope.
  • *
  • The most common types of search filters used for search * operations and their relative frequencies.
  • *
* It is designed to work with access log files using either the default log * format with separate request and response messages, as well as log files * in which the request and response details have been combined on the same * line. The log files to be processed should be provided as command-line * arguments. *

* The APIs demonstrated by this example include: *
    *
  • Access log parsing (from the * {@code com.unboundid.ldap.sdk.unboundidds.logs} package)
  • *
  • Argument parsing (from the {@code com.unboundid.util.args} * package)
  • *
*/ @NotMutable() @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class SummarizeAccessLog extends CommandLineTool implements Serializable { /** * The column at which long lines should be wrapped. */ private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; /** * The serial version UID for this serializable class. */ private static final long serialVersionUID = 7189168366509887130L; // Variables used for accessing argument information. @Nullable private ArgumentParser argumentParser; // An argument that may be used to indicate that the summarized output should // not be anonymized, and should include attribute values. @Nullable private BooleanArgument doNotAnonymize; // An argument that may be used to indicate that the log files are compressed. @Nullable private BooleanArgument isCompressed; // An argument that may be used to indicate that the log content is // JSON-formatted rather than text-formatted. @Nullable private BooleanArgument json; // An argument used to specify the encryption passphrase. @Nullable private FileArgument encryptionPassphraseFile; // An argument used to specify the maximum number of values to report for each // item. @Nullable private IntegerArgument reportCount; // The decimal format that will be used for this class. @NotNull private final DecimalFormat decimalFormat; // The total duration for log content, in milliseconds. private long logDurationMillis; // The total processing time for each type of operation. private double addProcessingDuration; private double bindProcessingDuration; private double compareProcessingDuration; private double deleteProcessingDuration; private double extendedProcessingDuration; private double modifyProcessingDuration; private double modifyDNProcessingDuration; private double searchProcessingDuration; // A variable used for tracking total work queue wait time. private long totalWorkQueueWaitTime; // A variable used for counting the number of messages of each type. private long numAbandons; private long numAdds; private long numBinds; private long numCompares; private long numConnects; private long numDeletes; private long numDisconnects; private long numExtended; private long numModifies; private long numModifyDNs; private long numSearches; private long numUnbinds; // The number of operations of each type that accessed uncached data. private long numUncachedAdds; private long numUncachedBinds; private long numUncachedCompares; private long numUncachedDeletes; private long numUncachedExtended; private long numUncachedModifies; private long numUncachedModifyDNs; private long numUncachedSearches; // The number of unindexed searches processed within the server. private long numUnindexedAttempts; private long numUnindexedFailed; private long numUnindexedSuccessful; // The number of request and response controls used. private long numRequestControls; private long numResponseControls; // Variables used for maintaining counts for common types of information. @NotNull private final HashMap searchEntryCounts; @NotNull private final HashMap ipAddressesByConnectionID; @NotNull private final HashMap addResultCodes; @NotNull private final HashMap bindResultCodes; @NotNull private final HashMap compareResultCodes; @NotNull private final HashMap deleteResultCodes; @NotNull private final HashMap extendedResultCodes; @NotNull private final HashMap modifyResultCodes; @NotNull private final HashMap modifyDNResultCodes; @NotNull private final HashMap searchResultCodes; @NotNull private final HashMap searchScopes; @NotNull private final HashMap authenticationTypes; @NotNull private final HashMap authzDNs; @NotNull private final HashMap bindFailuresByDN; @NotNull private final HashMap bindFailuresByIPAddress; @NotNull private final HashMap consecutiveFailedBindsByDN; @NotNull private final HashMap outstandingFailedBindDNs; @NotNull private final HashMap successfulBindDNs; @NotNull private final HashMap clientAddresses; @NotNull private final HashMap clientConnectionPolicies; @NotNull private final HashMap disconnectReasons; @NotNull private final HashMap extendedOperations; @NotNull private final HashMap filterComponentCounts; @NotNull private final HashMap filterTypes; @NotNull private final HashMap mostExpensiveFilters; @NotNull private final HashMap multiEntryFilters; @NotNull private final HashMap noEntryFilters; @NotNull private final HashMap oneEntryFilters; @NotNull private final HashMap preAuthzPrivilegesUsed; @NotNull private final HashMap privilegesMissing; @NotNull private final HashMap privilegesUsed; @NotNull private final HashMap requestControlOIDs; @NotNull private final HashMap responseControlOIDs; @NotNull private final HashMap searchBaseDNs; @NotNull private final HashMap tlsCipherSuites; @NotNull private final HashMap tlsProtocols; @NotNull private final HashMap unindexedFilters; @NotNull private final HashMap extendedOperationOIDsToNames; @NotNull private final HashSet processedRequests; @NotNull private final LinkedHashMap addProcessingTimes; @NotNull private final LinkedHashMap bindProcessingTimes; @NotNull private final LinkedHashMap compareProcessingTimes; @NotNull private final LinkedHashMap deleteProcessingTimes; @NotNull private final LinkedHashMap extendedProcessingTimes; @NotNull private final LinkedHashMap modifyProcessingTimes; @NotNull private final LinkedHashMap modifyDNProcessingTimes; @NotNull private final LinkedHashMap searchProcessingTimes; @NotNull private final LinkedHashMap workQueueWaitTimes; @NotNull private final LinkedHashSet filtersRepresentingPotentialInjectionAttempt; /** * Parse the provided command line arguments and perform the appropriate * processing. * * @param args The command line arguments provided to this program. */ public static void main(@NotNull final String[] args) { final ResultCode resultCode = main(args, System.out, System.err); if (resultCode != ResultCode.SUCCESS) { System.exit(resultCode.intValue()); } } /** * Parse the provided command line arguments and perform the appropriate * processing. * * @param args The command line arguments provided to this program. * @param outStream The output stream to which standard out should be * written. It may be {@code null} if output should be * suppressed. * @param errStream The output stream to which standard error should be * written. It may be {@code null} if error messages * should be suppressed. * * @return A result code indicating whether the processing was successful. */ @NotNull() public static ResultCode main(@NotNull final String[] args, @Nullable final OutputStream outStream, @Nullable final OutputStream errStream) { final SummarizeAccessLog summarizer = new SummarizeAccessLog(outStream, errStream); return summarizer.runTool(args); } /** * Creates a new instance of this tool. * * @param outStream The output stream to which standard out should be * written. It may be {@code null} if output should be * suppressed. * @param errStream The output stream to which standard error should be * written. It may be {@code null} if error messages * should be suppressed. */ public SummarizeAccessLog(@Nullable final OutputStream outStream, @Nullable final OutputStream errStream) { super(outStream, errStream); argumentParser = null; doNotAnonymize = null; isCompressed = null; json = null; encryptionPassphraseFile = null; reportCount = null; decimalFormat = new DecimalFormat("0.000"); logDurationMillis = 0L; addProcessingDuration = 0.0; bindProcessingDuration = 0.0; compareProcessingDuration = 0.0; deleteProcessingDuration = 0.0; extendedProcessingDuration = 0.0; modifyProcessingDuration = 0.0; modifyDNProcessingDuration = 0.0; searchProcessingDuration = 0.0; totalWorkQueueWaitTime = 0L; numAbandons = 0L; numAdds = 0L; numBinds = 0L; numCompares = 0L; numConnects = 0L; numDeletes = 0L; numDisconnects = 0L; numExtended = 0L; numModifies = 0L; numModifyDNs = 0L; numSearches = 0L; numUnbinds = 0L; numUncachedAdds = 0L; numUncachedBinds = 0L; numUncachedCompares = 0L; numUncachedDeletes = 0L; numUncachedExtended = 0L; numUncachedModifies = 0L; numUncachedModifyDNs = 0L; numUncachedSearches = 0L; numUnindexedAttempts = 0L; numUnindexedFailed = 0L; numUnindexedSuccessful = 0L; numRequestControls = 0L; numResponseControls = 0L; searchEntryCounts = new HashMap<>(StaticUtils.computeMapCapacity(10)); ipAddressesByConnectionID = new HashMap<>(StaticUtils.computeMapCapacity(100)); addResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); bindResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); compareResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); deleteResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); extendedResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); modifyResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); modifyDNResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); searchResultCodes = new HashMap<>(StaticUtils.computeMapCapacity(10)); searchScopes = new HashMap<>(StaticUtils.computeMapCapacity(4)); authenticationTypes = new HashMap<>(StaticUtils.computeMapCapacity(100)); authzDNs = new HashMap<>(StaticUtils.computeMapCapacity(100)); bindFailuresByDN = new HashMap<>(StaticUtils.computeMapCapacity(100)); bindFailuresByIPAddress = new HashMap<>(StaticUtils.computeMapCapacity(100)); outstandingFailedBindDNs = new HashMap<>(StaticUtils.computeMapCapacity(100)); successfulBindDNs = new HashMap<>(StaticUtils.computeMapCapacity(100)); clientAddresses = new HashMap<>(StaticUtils.computeMapCapacity(100)); clientConnectionPolicies = new HashMap<>(StaticUtils.computeMapCapacity(100)); disconnectReasons = new HashMap<>(StaticUtils.computeMapCapacity(100)); extendedOperations = new HashMap<>(StaticUtils.computeMapCapacity(10)); filterComponentCounts = new HashMap<>(StaticUtils.computeMapCapacity(10)); filterTypes = new HashMap<>(StaticUtils.computeMapCapacity(100)); mostExpensiveFilters = new HashMap<>(StaticUtils.computeMapCapacity(100)); multiEntryFilters = new HashMap<>(StaticUtils.computeMapCapacity(100)); noEntryFilters = new HashMap<>(StaticUtils.computeMapCapacity(100)); oneEntryFilters = new HashMap<>(StaticUtils.computeMapCapacity(100)); preAuthzPrivilegesUsed = new HashMap<>(StaticUtils.computeMapCapacity(100)); privilegesMissing = new HashMap<>(StaticUtils.computeMapCapacity(100)); privilegesUsed = new HashMap<>(StaticUtils.computeMapCapacity(100)); requestControlOIDs = new HashMap<>(StaticUtils.computeMapCapacity(100)); responseControlOIDs = new HashMap<>(StaticUtils.computeMapCapacity(100)); searchBaseDNs = new HashMap<>(StaticUtils.computeMapCapacity(100)); tlsCipherSuites = new HashMap<>(StaticUtils.computeMapCapacity(100)); tlsProtocols = new HashMap<>(StaticUtils.computeMapCapacity(100)); unindexedFilters = new HashMap<>(StaticUtils.computeMapCapacity(100)); consecutiveFailedBindsByDN = new HashMap<>(StaticUtils.computeMapCapacity(100)); extendedOperationOIDsToNames = new HashMap<>(StaticUtils.computeMapCapacity(100)); processedRequests = new HashSet<>(StaticUtils.computeMapCapacity(100)); addProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); bindProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); compareProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); deleteProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); extendedProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); modifyProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); modifyDNProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); searchProcessingTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); workQueueWaitTimes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(11)); filtersRepresentingPotentialInjectionAttempt = new LinkedHashSet<>(StaticUtils.computeMapCapacity(10)); populateProcessingTimeMap(addProcessingTimes); populateProcessingTimeMap(bindProcessingTimes); populateProcessingTimeMap(compareProcessingTimes); populateProcessingTimeMap(deleteProcessingTimes); populateProcessingTimeMap(extendedProcessingTimes); populateProcessingTimeMap(modifyProcessingTimes); populateProcessingTimeMap(modifyDNProcessingTimes); populateProcessingTimeMap(searchProcessingTimes); populateProcessingTimeMap(workQueueWaitTimes); } /** * Retrieves the name for this tool. * * @return The name for this tool. */ @Override() @NotNull() public String getToolName() { return "summarize-access-log"; } /** * Retrieves the description for this tool. * * @return The description for this tool. */ @Override() @NotNull() public String getToolDescription() { return "Examine one or more access log files from Ping Identity, " + "UnboundID, or Nokia/Alcatel-Lucent 8661 server products to display " + "a number of metrics about operations processed within the server."; } /** * Retrieves the version string for this tool. * * @return The version string for this tool. */ @Override() @NotNull() public String getToolVersion() { return Version.NUMERIC_VERSION_STRING; } /** * Retrieves the minimum number of unnamed trailing arguments that are * required. * * @return One, to indicate that at least one trailing argument (representing * the path to an access log file) must be provided. */ @Override() public int getMinTrailingArguments() { return 1; } /** * Retrieves the maximum number of unnamed trailing arguments that may be * provided for this tool. * * @return The maximum number of unnamed trailing arguments that may be * provided for this tool. */ @Override() public int getMaxTrailingArguments() { return -1; } /** * Retrieves a placeholder string that should be used for trailing arguments * in the usage information for this tool. * * @return A placeholder string that should be used for trailing arguments in * the usage information for this tool. */ @Override() @NotNull() public String getTrailingArgumentsPlaceholder() { return "{path}"; } /** * Indicates whether this tool should provide support for an interactive mode, * in which the tool offers a mode in which the arguments can be provided in * a text-driven menu rather than requiring them to be given on the command * line. If interactive mode is supported, it may be invoked using the * "--interactive" argument. Alternately, if interactive mode is supported * and {@link #defaultsToInteractiveMode()} returns {@code true}, then * interactive mode may be invoked by simply launching the tool without any * arguments. * * @return {@code true} if this tool supports interactive mode, or * {@code false} if not. */ @Override() public boolean supportsInteractiveMode() { return true; } /** * Indicates whether this tool defaults to launching in interactive mode if * the tool is invoked without any command-line arguments. This will only be * used if {@link #supportsInteractiveMode()} returns {@code true}. * * @return {@code true} if this tool defaults to using interactive mode if * launched without any command-line arguments, or {@code false} if * not. */ @Override() public boolean defaultsToInteractiveMode() { return true; } /** * Indicates whether this tool should provide arguments for redirecting output * to a file. If this method returns {@code true}, then the tool will offer * an "--outputFile" argument that will specify the path to a file to which * all standard output and standard error content will be written, and it will * also offer a "--teeToStandardOut" argument that can only be used if the * "--outputFile" argument is present and will cause all output to be written * to both the specified output file and to standard output. * * @return {@code true} if this tool should provide arguments for redirecting * output to a file, or {@code false} if not. */ @Override() protected boolean supportsOutputFile() { return true; } /** * Indicates whether this tool supports the use of a properties file for * specifying default values for arguments that aren't specified on the * command line. * * @return {@code true} if this tool supports the use of a properties file * for specifying default values for arguments that aren't specified * on the command line, or {@code false} if not. */ @Override() public boolean supportsPropertiesFile() { return true; } /** * Adds the command-line arguments supported for use with this tool to the * provided argument parser. The tool may need to retain references to the * arguments (and/or the argument parser, if trailing arguments are allowed) * to it in order to obtain their values for use in later processing. * * @param parser The argument parser to which the arguments are to be added. * * @throws ArgumentException If a problem occurs while adding any of the * tool-specific arguments to the provided * argument parser. */ @Override() public void addToolArguments(@NotNull final ArgumentParser parser) throws ArgumentException { // We need to save a reference to the argument parser so that we can get // the trailing arguments later. argumentParser = parser; // Add an argument that makes it possible to read a JSON-formatted access // log file. String description = "Indicates that the log file contains " + "JSON-formatted log messages rather than text-formatted messages."; json = new BooleanArgument(null, "json", description); parser.addArgument(json); // Add an argument that makes it possible to read a compressed log file. // Note that this argument is no longer needed for dealing with compressed // files, since the tool will automatically detect whether a file is // compressed. However, the argument is still provided for the purpose of // backward compatibility. description = "Indicates that the log file is compressed."; isCompressed = new BooleanArgument('c', "isCompressed", description); isCompressed.addLongIdentifier("is-compressed", true); isCompressed.addLongIdentifier("compressed", true); isCompressed.setHidden(true); parser.addArgument(isCompressed); // Add an argument that indicates that the tool should read the encryption // passphrase from a file. description = "Indicates that the log file is encrypted and that the " + "encryption passphrase is contained in the specified file. If " + "the log data is encrypted and this argument is not provided, then " + "the tool will interactively prompt for the encryption passphrase."; encryptionPassphraseFile = new FileArgument(null, "encryptionPassphraseFile", false, 1, null, description, true, true, true, false); encryptionPassphraseFile.addLongIdentifier("encryption-passphrase-file", true); encryptionPassphraseFile.addLongIdentifier("encryptionPasswordFile", true); encryptionPassphraseFile.addLongIdentifier("encryption-password-file", true); parser.addArgument(encryptionPassphraseFile); // Add an argument that indicates the number of values to display for each // item being summarized. description = "The number of values to display for each item being " + "summarized. A value of zero indicates that all items should be " + "displayed. If this is not provided, a default value of 20 will " + "be used."; reportCount = new IntegerArgument(null, "reportCount", false, 0, null, description, 0, Integer.MAX_VALUE, 20); reportCount.addLongIdentifier("report-count", true); reportCount.addLongIdentifier("maximumCount", true); reportCount.addLongIdentifier("maximum-count", true); reportCount.addLongIdentifier("maxCount", true); reportCount.addLongIdentifier("max-count", true); reportCount.addLongIdentifier("count", true); parser.addArgument(reportCount); // Add an argument that indicates that the output should not be anonymized. description = "Do not anonymize the output, but include actual attribute " + "values in filters and DNs. This will also have the effect of " + "de-generifying those values, so output including the most common " + "filters and DNs in some category will be specific instances of " + "those filters and DNs instead of generic patterns."; doNotAnonymize = new BooleanArgument(null, "doNotAnonymize", 1, description); doNotAnonymize.addLongIdentifier("do-not-anonymize", true); doNotAnonymize.addLongIdentifier("deAnonymize", true); doNotAnonymize.addLongIdentifier("de-anonymize", true); parser.addArgument(doNotAnonymize); } /** * Performs any necessary processing that should be done to ensure that the * provided set of command-line arguments were valid. This method will be * called after the basic argument parsing has been performed and immediately * before the {@link #doToolProcessing} method is invoked. * * @throws ArgumentException If there was a problem with the command-line * arguments provided to this program. */ @Override() public void doExtendedArgumentValidation() throws ArgumentException { // Make sure that at least one access log file path was provided. final List trailingArguments = argumentParser.getTrailingArguments(); if ((trailingArguments == null) || trailingArguments.isEmpty()) { throw new ArgumentException("No access log file paths were provided."); } } /** * Performs the core set of processing for this tool. * * @return A result code that indicates whether the processing completed * successfully. */ @Override() @NotNull() public ResultCode doToolProcessing() { int displayCount = reportCount.getValue(); if (displayCount <= 0) { displayCount = Integer.MAX_VALUE; } String encryptionPassphrase = null; if (encryptionPassphraseFile.isPresent()) { try { encryptionPassphrase = ToolUtils.readEncryptionPassphraseFromFile( encryptionPassphraseFile.getValue()); } catch (final LDAPException e) { Debug.debugException(e); err(e.getMessage()); return e.getResultCode(); } } long logLines = 0L; for (final String path : argumentParser.getTrailingArguments()) { final File f = new File(path); out("Examining access log ", f.getAbsolutePath()); AccessLogReader reader = null; InputStream inputStream = null; try { inputStream = new FileInputStream(f); final ObjectPair p = ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream, encryptionPassphrase, (! encryptionPassphraseFile.isPresent()), "Log file '" + path + "' is encrypted. Please enter the " + "encryption passphrase:", "ERROR: The provided passphrase was incorrect.", getOut(), getErr()); inputStream = p.getFirst(); if ((p.getSecond() != null) && (encryptionPassphrase == null)) { encryptionPassphrase = p.getSecond(); } if (isCompressed.isPresent()) { inputStream = new GZIPInputStream(inputStream); } else { inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); } if (json.isPresent()) { reader = new JSONAccessLogReader(inputStream); } else { reader = new TextFormattedAccessLogReader(inputStream); } } catch (final Exception e) { Debug.debugException(e); err("Unable to open access log file ", f.getAbsolutePath(), ": ", StaticUtils.getExceptionMessage(e)); return ResultCode.LOCAL_ERROR; } finally { if ((reader == null) && (inputStream != null)) { try { inputStream.close(); } catch (final Exception e) { Debug.debugException(e); } } } long startTime = 0L; long stopTime = 0L; while (true) { final AccessLogMessage msg; try { msg = reader.readMessage(); } catch (final IOException ioe) { Debug.debugException(ioe); err("Error reading from access log file ", f.getAbsolutePath(), ": ", StaticUtils.getExceptionMessage(ioe)); if ((ioe.getCause() != null) && (ioe.getCause() instanceof BadPaddingException)) { err("This error is likely because the log is encrypted and the " + "server still has the log file open. It is recommended " + "that you only try to examine encrypted logs after they " + "have been rotated. You can use the rotate-log tool to " + "force a rotation at any time. Attempting to proceed with " + "just the data that was successfully read."); break; } else { return ResultCode.LOCAL_ERROR; } } catch (final LogException le) { Debug.debugException(le); err("Encountered an error while attempting to parse a line in" + "access log file ", f.getAbsolutePath(), ": ", StaticUtils.getExceptionMessage(le)); continue; } if (msg == null) { break; } logLines++; stopTime = msg.getTimestamp().getTime(); if (startTime == 0L) { startTime = stopTime; } switch (msg.getMessageType()) { case CONNECT: processConnect((ConnectAccessLogMessage) msg); break; case SECURITY_NEGOTIATION: processSecurityNegotiation( (SecurityNegotiationAccessLogMessage) msg); break; case DISCONNECT: processDisconnect((DisconnectAccessLogMessage) msg); break; case REQUEST: switch (((OperationRequestAccessLogMessage) msg).getOperationType()) { case ABANDON: processAbandonRequest((AbandonRequestAccessLogMessage) msg); break; case EXTENDED: processExtendedRequest((ExtendedRequestAccessLogMessage) msg); break; case SEARCH: processSearchRequest((SearchRequestAccessLogMessage) msg); break; case UNBIND: processUnbindRequest((UnbindRequestAccessLogMessage) msg); break; } break; case RESULT: switch (((OperationRequestAccessLogMessage) msg).getOperationType()) { case ADD: processAddResult((AddResultAccessLogMessage) msg); break; case BIND: processBindResult((BindResultAccessLogMessage) msg); break; case COMPARE: processCompareResult((CompareResultAccessLogMessage) msg); break; case DELETE: processDeleteResult((DeleteResultAccessLogMessage) msg); break; case EXTENDED: processExtendedResult((ExtendedResultAccessLogMessage) msg); break; case MODIFY: processModifyResult((ModifyResultAccessLogMessage) msg); break; case MODDN: processModifyDNResult((ModifyDNResultAccessLogMessage) msg); break; case SEARCH: processSearchResult((SearchResultAccessLogMessage) msg); break; } break; case ASSURANCE_COMPLETE: case CLIENT_CERTIFICATE: case ENTRY_REBALANCING_REQUEST: case ENTRY_REBALANCING_RESULT: case FORWARD: case FORWARD_FAILED: case ENTRY: case REFERENCE: default: // Nothing needs to be done for these message types. } } try { reader.close(); } catch (final Exception e) { Debug.debugException(e); } logDurationMillis += (stopTime - startTime); // If there are any outstanding authentication failures, then update the // set of consecutive failures as appropriate. for (final Map.Entry e : outstandingFailedBindDNs.entrySet()) { final String dn = e.getKey(); final AtomicLong outstandingFailureCount = e.getValue(); final AtomicLong consecutiveFailures = consecutiveFailedBindsByDN.get(dn); if ((consecutiveFailures == null) || (outstandingFailureCount.get() > consecutiveFailures.get())) { consecutiveFailedBindsByDN.put(dn, outstandingFailureCount); } } outstandingFailedBindDNs.clear(); } final int numFiles = argumentParser.getTrailingArguments().size(); out(); out("Examined ", logLines, " lines in ", numFiles, ((numFiles == 1) ? " file" : " files"), " covering a total duration of ", StaticUtils.millisToHumanReadableDuration(logDurationMillis)); if (logLines == 0) { return ResultCode.SUCCESS; } out(); final double logDurationSeconds = logDurationMillis / 1_000.0; final double connectsPerSecond = numConnects / logDurationSeconds; final double disconnectsPerSecond = numDisconnects / logDurationSeconds; out("Total connections established: ", numConnects, " (", decimalFormat.format(connectsPerSecond), "/second)"); out("Total disconnects: ", numDisconnects, " (", decimalFormat.format(disconnectsPerSecond), "/second)"); printCounts(clientAddresses, "Most common client addresses:", "address", "addresses"); printCounts(clientConnectionPolicies, "Most common client connection policies:", "policy", "policies"); printCounts(tlsProtocols, "Most common TLS protocol versions:", "version", "versions"); printCounts(tlsCipherSuites, "Most common TLS cipher suites:", "cipher suite", "cipher suites"); printCounts(disconnectReasons, "Most common disconnect reasons:", "reason", "reasons"); final long totalOps = numAbandons + numAdds + numBinds + numCompares + numDeletes + numExtended + numModifies + numModifyDNs + numSearches + numUnbinds; final long totalResults = totalOps - numAbandons - numUnbinds; if (totalOps > 0) { final double percentAbandon = 100.0 * numAbandons / totalOps; final double percentAdd = 100.0 * numAdds / totalOps; final double percentBind = 100.0 * numBinds / totalOps; final double percentCompare = 100.0 * numCompares / totalOps; final double percentDelete = 100.0 * numDeletes / totalOps; final double percentExtended = 100.0 * numExtended / totalOps; final double percentModify = 100.0 * numModifies / totalOps; final double percentModifyDN = 100.0 * numModifyDNs / totalOps; final double percentSearch = 100.0 * numSearches / totalOps; final double percentUnbind = 100.0 * numUnbinds / totalOps; final double abandonsPerSecond = numAbandons / logDurationSeconds; final double addsPerSecond = numAdds / logDurationSeconds; final double bindsPerSecond = numBinds / logDurationSeconds; final double comparesPerSecond = numCompares / logDurationSeconds; final double deletesPerSecond = numDeletes / logDurationSeconds; final double extendedPerSecond = numExtended / logDurationSeconds; final double modifiesPerSecond = numModifies / logDurationSeconds; final double modifyDNsPerSecond = numModifyDNs / logDurationSeconds; final double searchesPerSecond = numSearches / logDurationSeconds; final double unbindsPerSecond = numUnbinds / logDurationSeconds; out(); out("Total operations examined: ", totalOps); out("Abandon operations examined: ", numAbandons, " (", decimalFormat.format(percentAbandon), "%, ", decimalFormat.format(abandonsPerSecond), "/second)"); out("Add operations examined: ", numAdds, " (", decimalFormat.format(percentAdd), "%, ", decimalFormat.format(addsPerSecond), "/second)"); out("Bind operations examined: ", numBinds, " (", decimalFormat.format(percentBind), "%, ", decimalFormat.format(bindsPerSecond), "/second)"); out("Compare operations examined: ", numCompares, " (", decimalFormat.format(percentCompare), "%, ", decimalFormat.format(comparesPerSecond), "/second)"); out("Delete operations examined: ", numDeletes, " (", decimalFormat.format(percentDelete), "%, ", decimalFormat.format(deletesPerSecond), "/second)"); out("Extended operations examined: ", numExtended, " (", decimalFormat.format(percentExtended), "%, ", decimalFormat.format(extendedPerSecond), "/second)"); out("Modify operations examined: ", numModifies, " (", decimalFormat.format(percentModify), "%, ", decimalFormat.format(modifiesPerSecond), "/second)"); out("Modify DN operations examined: ", numModifyDNs, " (", decimalFormat.format(percentModifyDN), "%, ", decimalFormat.format(modifyDNsPerSecond), "/second)"); out("Search operations examined: ", numSearches, " (", decimalFormat.format(percentSearch), "%, ", decimalFormat.format(searchesPerSecond), "/second)"); out("Unbind operations examined: ", numUnbinds, " (", decimalFormat.format(percentUnbind), "%, ", decimalFormat.format(unbindsPerSecond), "/second)"); final double totalProcessingDuration = addProcessingDuration + bindProcessingDuration + compareProcessingDuration + deleteProcessingDuration + extendedProcessingDuration + modifyProcessingDuration + modifyDNProcessingDuration + searchProcessingDuration; out(); out("Average operation processing duration: ", decimalFormat.format(totalProcessingDuration / totalOps), "ms"); if (numAdds > 0) { out("Average add operation processing duration: ", decimalFormat.format(addProcessingDuration / numAdds), "ms"); } if (numBinds > 0) { out("Average bind operation processing duration: ", decimalFormat.format(bindProcessingDuration / numBinds), "ms"); } if (numCompares > 0) { out("Average compare operation processing duration: ", decimalFormat.format(compareProcessingDuration / numCompares), "ms"); } if (numDeletes > 0) { out("Average delete operation processing duration: ", decimalFormat.format(deleteProcessingDuration / numDeletes), "ms"); } if (numExtended > 0) { out("Average extended operation processing duration: ", decimalFormat.format(extendedProcessingDuration / numExtended), "ms"); } if (numModifies > 0) { out("Average modify operation processing duration: ", decimalFormat.format(modifyProcessingDuration / numModifies), "ms"); } if (numModifyDNs > 0) { out("Average modify DN operation processing duration: ", decimalFormat.format(modifyDNProcessingDuration / numModifyDNs), "ms"); } if (numSearches > 0) { out("Average search operation processing duration: ", decimalFormat.format(searchProcessingDuration / numSearches), "ms"); } printProcessingTimeHistogram("add", numAdds, addProcessingTimes); printProcessingTimeHistogram("bind", numBinds, bindProcessingTimes); printProcessingTimeHistogram("compare", numCompares, compareProcessingTimes); printProcessingTimeHistogram("delete", numDeletes, deleteProcessingTimes); printProcessingTimeHistogram("extended", numExtended, extendedProcessingTimes); printProcessingTimeHistogram("modify", numModifies, modifyProcessingTimes); printProcessingTimeHistogram("modify DN", numModifyDNs, modifyDNProcessingTimes); printProcessingTimeHistogram("search", numSearches, searchProcessingTimes); if (totalWorkQueueWaitTime > 0L) { out(); out("Average work queue wait time: ", decimalFormat.format(totalWorkQueueWaitTime / totalResults), "ms"); printHistogram("Count of operations by work queue wait time:", totalResults, workQueueWaitTimes); } printResultCodeCounts(addResultCodes, "add"); printResultCodeCounts(bindResultCodes, "bind"); printResultCodeCounts(compareResultCodes, "compare"); printResultCodeCounts(deleteResultCodes, "delete"); printResultCodeCounts(extendedResultCodes, "extended"); printResultCodeCounts(modifyResultCodes, "modify"); printResultCodeCounts(modifyDNResultCodes, "modify DN"); printResultCodeCounts(searchResultCodes, "search"); printCounts(preAuthzPrivilegesUsed, "Most common pre-authorization privileges used:", "privilege", "privileges"); printCounts(privilegesUsed, "Most common privileges used:", "privilege", "privileges"); printCounts(privilegesMissing, "Most common missing privileges:", "privilege", "privileges"); printCounts(successfulBindDNs, "Most common bind DNs used in successful authentication attempts:", "DN", "DNs"); printCounts(bindFailuresByDN, "Most common bind DNs used in failed authentication attempts:", "DN", "DNs"); printCounts(bindFailuresByIPAddress, "Most common IP addresses used in failed authentication attempts:", "IP", "IPs"); if (doNotAnonymize.isPresent()) { printCounts(consecutiveFailedBindsByDN, "Bind DNs with the most consecutive authentication failures:", "DN", "DNs"); } printCounts(authenticationTypes, "Most common authentication types:", "authentication type", "authentication types"); long numResultsWithAuthzID = 0L; for (final AtomicLong l : authzDNs.values()) { numResultsWithAuthzID += l.get(); } out(); final double percentWithAuthzID = 100.0 * numResultsWithAuthzID / totalOps; out("Number of operations with an alternate authorization identity: ", numResultsWithAuthzID, " (", decimalFormat.format(percentWithAuthzID), "%)"); printCounts(authzDNs, "Most common alternate authorization identity DNs:", "DN", "DNs"); if (! requestControlOIDs.isEmpty()) { final List> controlCounts = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(requestControlOIDs, controlCounts, displayCount, skippedWithSameCount, skippedWithLowerCount); out(); out("Most common request control types:"); long count = -1L; for (final ObjectPair p : controlCounts) { count = p.getSecond(); final double percent = 100.0 * count / numRequestControls; final String oid = p.getFirst(); final OIDRegistryItem item = OIDRegistry.getDefault().get(oid); if (item == null) { out(p.getFirst(), ": ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } else { out(p.getFirst(), " (", item.getName(), "): ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional " + getSingularOrPlural(skippedWithSameCount.get(), "control", "controls") + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional " + getSingularOrPlural(skippedWithLowerCount.get(), "control", "controls") + " with a count that is less than " + count + " }"); } } if (! responseControlOIDs.isEmpty()) { final List> controlCounts = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(responseControlOIDs, controlCounts, displayCount, skippedWithSameCount, skippedWithLowerCount); out(); out("Most common response control types:"); long count = -1L; for (final ObjectPair p : controlCounts) { count = p.getSecond(); final double percent = 100.0 * count / numResponseControls; final String oid = p.getFirst(); final OIDRegistryItem item = OIDRegistry.getDefault().get(oid); if (item == null) { out(p.getFirst(), ": ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } else { out(p.getFirst(), " (", item.getName(), "): ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional " + getSingularOrPlural(skippedWithSameCount.get(), "control", "controls") + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional " + getSingularOrPlural(skippedWithLowerCount.get(), "control", "controls") + " with a count that is less than " + count + " }"); } } if (! extendedOperations.isEmpty()) { final List> extOpCounts = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(extendedOperations, extOpCounts, displayCount, skippedWithSameCount, skippedWithLowerCount); out(); out("Most common extended operation types:"); long count = -1L; for (final ObjectPair p : extOpCounts) { count = p.getSecond(); final double percent = 100.0 * count / numExtended; final String oid = p.getFirst(); final String name = extendedOperationOIDsToNames.get(oid); if (name == null) { out(p.getFirst(), ": ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } else { out(p.getFirst(), " (", name, "): ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional extended " + getSingularOrPlural(skippedWithSameCount.get(), "operation", "operations") + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional extended " + getSingularOrPlural(skippedWithLowerCount.get(), "operation", "operations") + " with a count that is less than " + count + " }"); } } out(); out("Number of unindexed search attempts: ", numUnindexedAttempts); out("Number of successfully-completed unindexed searches: ", numUnindexedSuccessful); out("Number of failed unindexed searches: ", numUnindexedFailed); printCounts(unindexedFilters, "Most common unindexed search filters:", "filter", "filters"); if (! searchScopes.isEmpty()) { final List> scopeCounts = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(searchScopes, scopeCounts, displayCount, skippedWithSameCount, skippedWithLowerCount); out(); out("Most common search scopes:"); long count = -1L; for (final ObjectPair p : scopeCounts) { count = p.getSecond(); final double percent = 100.0 * count / numSearches; out(p.getFirst().getName().toLowerCase(), " (", p.getFirst().intValue(), "): ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional " + getSingularOrPlural(skippedWithSameCount.get(), "scope", "scopes") + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional " + getSingularOrPlural(skippedWithLowerCount.get(), "scope", "scopes") + " with a count that is less than " + count + " }"); } } if (! searchEntryCounts.isEmpty()) { final List> entryCounts = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(searchEntryCounts, entryCounts, displayCount, skippedWithSameCount, skippedWithLowerCount); out(); out("Most common search entry counts:"); long count = -1L; for (final ObjectPair p : entryCounts) { count = p.getSecond(); final double percent = 100.0 * count / numSearches; out(p.getFirst(), " matching ", getSingularOrPlural(p.getFirst(), "entry", "entries"), ": ", p.getSecond(), " (", decimalFormat.format(percent), "%)"); } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional entry " + getSingularOrPlural(skippedWithSameCount.get(), "count", "counts") + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional entry " + getSingularOrPlural(skippedWithLowerCount.get(), "count", "counts") + " with a count that is less than " + count + " }"); } } printCounts(searchBaseDNs, "Most common base DNs for searches with a non-base scope:", "base DN", "base DNs"); printCounts(filterTypes, "Most common filters for searches with a non-base scope:", "filter", "filters"); printCounts(filterComponentCounts, "Most common search filter component counts:", "filter", "filters"); if (doNotAnonymize.isPresent() && (! filtersRepresentingPotentialInjectionAttempt.isEmpty())) { out(); wrapOut(0, WRAP_COLUMN, "Search filters that may indicate an unsuccessful injection " + "attempt. These include filters with an assertion value " + "that contains one or more of the following: parentheses, " + "ampersands, pipes, single quotes, double quotes, or the " + "words 'select' and 'from':"); for (final Filter f : filtersRepresentingPotentialInjectionAttempt) { out("* " + f.toString()); } } if (numSearches > 0L) { long numSearchesMatchingNoEntries = 0L; for (final AtomicLong l : noEntryFilters.values()) { numSearchesMatchingNoEntries += l.get(); } out(); final double noEntryPercent = 100.0 * numSearchesMatchingNoEntries / numSearches; out("Number of searches matching no entries: ", numSearchesMatchingNoEntries, " (", decimalFormat.format(noEntryPercent), "%)"); printCounts(noEntryFilters, "Most common filters for searches matching no entries:", "filter", "filters"); long numSearchesMatchingOneEntry = 0L; for (final AtomicLong l : oneEntryFilters.values()) { numSearchesMatchingOneEntry += l.get(); } out(); final double oneEntryPercent = 100.0 * numSearchesMatchingOneEntry / numSearches; out("Number of searches matching one entry: ", numSearchesMatchingOneEntry, " (", decimalFormat.format(oneEntryPercent), "%)"); printCounts(oneEntryFilters, "Most common filters for searches matching one entry:", "filter", "filters"); long numSearchesMatchingMultipleEntries = 0L; for (final AtomicLong l : multiEntryFilters.values()) { numSearchesMatchingMultipleEntries += l.get(); } out(); final double multiEntryPercent = 100.0 * numSearchesMatchingMultipleEntries / numSearches; out("Number of searches matching multiple entries: ", numSearchesMatchingMultipleEntries, " (", decimalFormat.format(multiEntryPercent), "%)"); printCounts(multiEntryFilters, "Most common filters for searches matching multiple entries:", "filter", "filters"); } } if (! mostExpensiveFilters.isEmpty()) { final List> filterDurations = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(mostExpensiveFilters, filterDurations, displayCount, skippedWithSameCount, skippedWithLowerCount); out(); out("Filters for searches with the longest processing times:"); String durationStr = ""; for (final ObjectPair p : filterDurations) { final long durationMicros = p.getSecond(); final double durationMillis = durationMicros / 1_000.0; durationStr = decimalFormat.format(durationMillis) + " ms"; out(p.getFirst(), ": ", durationStr); } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional " + getSingularOrPlural(skippedWithSameCount.get(), "filter", "filters") + " with a duration of " + durationStr + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional " + getSingularOrPlural(skippedWithLowerCount.get(), "filter", "filters") + " with a duration that is less than " + durationStr + " }"); } } final long totalUncached = numUncachedAdds + numUncachedBinds + numUncachedCompares + numUncachedDeletes + numUncachedExtended + numUncachedModifies + numUncachedModifyDNs + numUncachedSearches; if (totalUncached > 0L) { out(); out("Operations accessing uncached data:"); printUncached("Add", numUncachedAdds, numAdds); printUncached("Bind", numUncachedBinds, numBinds); printUncached("Compare", numUncachedCompares, numCompares); printUncached("Delete", numUncachedDeletes, numDeletes); printUncached("Extended", numUncachedExtended, numExtended); printUncached("Modify", numUncachedModifies, numModifies); printUncached("Modify DN", numUncachedModifyDNs, numModifyDNs); printUncached("Search", numUncachedSearches, numSearches); } return ResultCode.SUCCESS; } /** * Retrieves a set of information that may be used to generate example usage * information. Each element in the returned map should consist of a map * between an example set of arguments and a string that describes the * behavior of the tool when invoked with that set of arguments. * * @return A set of information that may be used to generate example usage * information. It may be {@code null} or empty if no example usage * information is available. */ @Override() @NotNull() public LinkedHashMap getExampleUsages() { final LinkedHashMap examples = new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); final String[] args = { "/ds/logs/access" }; final String description = "Analyze the contents of the /ds/logs/access access log file."; examples.put(args, description); return examples; } /** * Populates the provided processing time map with an initial set of values. * * @param m The processing time map to be populated. */ private static void populateProcessingTimeMap( @NotNull final HashMap m) { m.put(1L, new AtomicLong(0L)); m.put(2L, new AtomicLong(0L)); m.put(3L, new AtomicLong(0L)); m.put(5L, new AtomicLong(0L)); m.put(10L, new AtomicLong(0L)); m.put(20L, new AtomicLong(0L)); m.put(30L, new AtomicLong(0L)); m.put(50L, new AtomicLong(0L)); m.put(100L, new AtomicLong(0L)); m.put(1_000L, new AtomicLong(0L)); m.put(2_000L, new AtomicLong(0L)); m.put(3_000L, new AtomicLong(0L)); m.put(5_000L, new AtomicLong(0L)); m.put(10_000L, new AtomicLong(0L)); m.put(20_000L, new AtomicLong(0L)); m.put(30_000L, new AtomicLong(0L)); m.put(60_000L, new AtomicLong(0L)); m.put(Long.MAX_VALUE, new AtomicLong(0L)); } /** * Performs any necessary processing for a connect message. * * @param m The log message to be processed. */ private void processConnect(@NotNull final ConnectAccessLogMessage m) { numConnects++; final String clientAddr = m.getSourceAddress(); if (clientAddr != null) { final Long connectionID = m.getConnectionID(); if (connectionID != null) { ipAddressesByConnectionID.put(connectionID, clientAddr); } AtomicLong count = clientAddresses.get(clientAddr); if (count == null) { count = new AtomicLong(0L); clientAddresses.put(clientAddr, count); } count.incrementAndGet(); } final String ccp = m.getClientConnectionPolicy(); if (ccp != null) { AtomicLong l = clientConnectionPolicies.get(ccp); if (l == null) { l = new AtomicLong(0L); clientConnectionPolicies.put(ccp, l); } l.incrementAndGet(); } } /** * Performs any necessary processing for a security negotiation message. * * @param m The log message to be processed. */ private void processSecurityNegotiation( @NotNull final SecurityNegotiationAccessLogMessage m) { final String protocol = m.getProtocol(); if (protocol != null) { AtomicLong l = tlsProtocols.get(protocol); if (l == null) { l = new AtomicLong(0L); tlsProtocols.put(protocol, l); } l.incrementAndGet(); } final String cipherSuite = m.getCipher(); if (cipherSuite != null) { AtomicLong l = tlsCipherSuites.get(cipherSuite); if (l == null) { l = new AtomicLong(0L); tlsCipherSuites.put(cipherSuite, l); } l.incrementAndGet(); } } /** * Performs any necessary processing for a disconnect message. * * @param m The log message to be processed. */ private void processDisconnect(@NotNull final DisconnectAccessLogMessage m) { numDisconnects++; final Long connectionID = m.getConnectionID(); if (connectionID != null) { ipAddressesByConnectionID.remove(connectionID); } final String reason = m.getDisconnectReason(); if (reason != null) { AtomicLong l = disconnectReasons.get(reason); if (l == null) { l = new AtomicLong(0L); disconnectReasons.put(reason, l); } l.incrementAndGet(); } } /** * Performs any necessary processing for an abandon request message. * * @param m The log message to be processed. */ private void processAbandonRequest( @NotNull final AbandonRequestAccessLogMessage m) { numAbandons++; } /** * Performs any necessary processing for an extended request message. * * @param m The log message to be processed. */ private void processExtendedRequest( @NotNull final ExtendedRequestAccessLogMessage m) { processedRequests.add(m.getConnectionID() + "-" + m.getOperationID()); processExtendedRequestInternal(m); } /** * Performs the internal processing for an extended request message. * * @param m The log message to be processed. */ private void processExtendedRequestInternal( @NotNull final ExtendedRequestAccessLogMessage m) { final String oid = m.getRequestOID(); if (oid != null) { AtomicLong l = extendedOperations.get(oid); if (l == null) { l = new AtomicLong(0L); extendedOperations.put(oid, l); } l.incrementAndGet(); final String requestType = m.getRequestType(); if ((requestType != null) && (! extendedOperationOIDsToNames.containsKey(oid))) { extendedOperationOIDsToNames.put(oid, requestType); } } } /** * Performs any necessary processing for a search request message. * * @param m The log message to be processed. */ private void processSearchRequest( @NotNull final SearchRequestAccessLogMessage m) { processedRequests.add(m.getConnectionID() + "-" + m.getOperationID()); processSearchRequestInternal(m); } /** * Performs any necessary processing for a search request message. * * @param m The log message to be processed. */ private void processSearchRequestInternal( @NotNull final SearchRequestAccessLogMessage m) { final SearchScope scope = m.getScope(); if (scope != null) { AtomicLong scopeCount = searchScopes.get(scope); if (scopeCount == null) { scopeCount = new AtomicLong(0L); searchScopes.put(scope, scopeCount); } scopeCount.incrementAndGet(); if (! scope.equals(SearchScope.BASE)) { final String filterString = prepareFilter(m.getFilter()); if (filterString != null) { AtomicLong filterCount = filterTypes.get(filterString); if (filterCount == null) { filterCount = new AtomicLong(0L); filterTypes.put(filterString, filterCount); } filterCount.incrementAndGet(); final String baseDN = getDNString(m.getBaseDN()); if (baseDN != null) { AtomicLong baseDNCount = searchBaseDNs.get(baseDN); if (baseDNCount == null) { baseDNCount = new AtomicLong(0L); searchBaseDNs.put(baseDN, baseDNCount); } baseDNCount.incrementAndGet(); } } } } final String filterString = m.getFilter(); if (filterString != null) { try { final Filter filter = Filter.create(filterString); if (mayRepresentInjectionAttempt(filter)) { filtersRepresentingPotentialInjectionAttempt.add(filter); } final int numComponents = countComponents(filter); final String label; if (numComponents == 1) { label = "1 component"; } else { label = numComponents + " components"; } AtomicLong count = filterComponentCounts.get(label); if (count == null) { count = new AtomicLong(0L); filterComponentCounts.put(label, count); } count.incrementAndGet(); } catch (final Exception e) { Debug.debugException(e); } } } /** * Indicates whether the provided search filter may represent an injection * attempt. Filters that may represent injection attempts include: *
    *
  • Filters with assertion values that contain parentheses, ampersands, * pipes, or single or double quotes.
  • *
  • Filters that contain the words "select" and "from".
  • *
* * @param filter The filter to examine. It must not be {@code null}. * * @return {@code true} if the provided filter may represent an injection * attempt, or {@code false} if not. */ static boolean mayRepresentInjectionAttempt(@NotNull final Filter filter) { switch (filter.getFilterType()) { case Filter.FILTER_TYPE_AND: case Filter.FILTER_TYPE_OR: for (final Filter f : filter.getComponents()) { if (mayRepresentInjectionAttempt(f)) { return true; } } return false; case Filter.FILTER_TYPE_NOT: return mayRepresentInjectionAttempt(filter.getNOTComponent()); case Filter.FILTER_TYPE_EQUALITY: case Filter.FILTER_TYPE_GREATER_OR_EQUAL: case Filter.FILTER_TYPE_LESS_OR_EQUAL: case Filter.FILTER_TYPE_APPROXIMATE_MATCH: case Filter.FILTER_TYPE_EXTENSIBLE_MATCH: return mayRepresentInjectionAttempt(filter.getAssertionValue()); case Filter.FILTER_TYPE_SUBSTRING: final String[] subAnyStrings = filter.getSubAnyStrings(); if (subAnyStrings != null) { for (final String subAnyString : subAnyStrings) { if (mayRepresentInjectionAttempt(subAnyString)) { return true; } } } return mayRepresentInjectionAttempt(filter.getSubInitialString()) || mayRepresentInjectionAttempt(filter.getSubFinalString()); case Filter.FILTER_TYPE_PRESENCE: default: return false; } } /** * Indicates whether the provided string (which should be a filter assertion * value or substring component) may represent an injection attempt. * * @param value The value for which to make the determination. It may * optionally be {@code null}. * * @return {@code true} if the provided value may represent an injection * attempt, or {@code false} if not. */ private static boolean mayRepresentInjectionAttempt( @Nullable final String value) { if (value == null) { return false; } final String lowerValue = StaticUtils.toLowerCase(value); return (lowerValue.contains("(") || lowerValue.contains(")") || lowerValue.contains("&") || lowerValue.contains("|") || lowerValue.contains("\"") || lowerValue.contains("'") || ((lowerValue.contains("select") && lowerValue.contains("from")))); } /** * Counts the number of components in the specified filter. Presence, * equality, substring, greater-or-equal, less-or-equal, approximate-match, * and extensible-match filters will all be considered a single component. * AND and OR filters will be one plus the aggregate component count for each * of the components they contain. NOT filters will be one plus the component * count for the filter it contains. * * @param filter The filter for which to count the number of components. It * must not be {@code null}. * * @return The number of components in the specified filter. */ static int countComponents(@NotNull final Filter filter) { switch (filter.getFilterType()) { case Filter.FILTER_TYPE_AND: case Filter.FILTER_TYPE_OR: int count = 1; for (final Filter f : filter.getComponents()) { count += countComponents(f); } return count; case Filter.FILTER_TYPE_NOT: return 1 + countComponents(filter.getNOTComponent()); case Filter.FILTER_TYPE_PRESENCE: case Filter.FILTER_TYPE_EQUALITY: case Filter.FILTER_TYPE_SUBSTRING: case Filter.FILTER_TYPE_GREATER_OR_EQUAL: case Filter.FILTER_TYPE_LESS_OR_EQUAL: case Filter.FILTER_TYPE_APPROXIMATE_MATCH: case Filter.FILTER_TYPE_EXTENSIBLE_MATCH: default: return 1; } } /** * Performs any necessary processing for an unbind request message. * * @param m The log message to be processed. */ private void processUnbindRequest( @NotNull final UnbindRequestAccessLogMessage m) { numUnbinds++; } /** * Performs any necessary processing for an add result message. * * @param m The log message to be processed. */ private void processAddResult(@NotNull final AddResultAccessLogMessage m) { numAdds++; updateCommonResult(m); updateResultCodeCount(m.getResultCode(), addResultCodes); addProcessingDuration += doubleValue(m.getProcessingTimeMillis(), addProcessingTimes); final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedAdds++; } updateAuthzCount(m.getAlternateAuthorizationDN()); } /** * Performs any necessary processing for a bind result message. * * @param m The log message to be processed. */ private void processBindResult(@NotNull final BindResultAccessLogMessage m) { numBinds++; updateCommonResult(m); if (m.getAuthenticationType() != null) { final String authType; switch (m.getAuthenticationType()) { case SIMPLE: authType = "Simple"; break; case SASL: final String saslMechanism = m.getSASLMechanismName(); if (saslMechanism == null) { authType = "SASL {unknown mechanism}"; } else { authType = "SASL " + saslMechanism; } break; case INTERNAL: authType = "Internal"; break; default: authType = m.getAuthenticationType().name(); break; } AtomicLong l = authenticationTypes.get(authType); if (l == null) { l = new AtomicLong(0L); authenticationTypes.put(authType, l); } l.incrementAndGet(); } updateResultCodeCount(m.getResultCode(), bindResultCodes); bindProcessingDuration += doubleValue(m.getProcessingTimeMillis(), bindProcessingTimes); String authenticationDN = getDNString(m.getAuthenticationDN()); if (m.getResultCode() == ResultCode.SUCCESS) { if (authenticationDN != null) { AtomicLong l = successfulBindDNs.get(authenticationDN); if (l == null) { l = new AtomicLong(0L); successfulBindDNs.put(authenticationDN, l); } l.incrementAndGet(); final AtomicLong outstandingFailures = outstandingFailedBindDNs.remove(authenticationDN); if (outstandingFailures != null) { final AtomicLong consecutiveFailures = consecutiveFailedBindsByDN.get(authenticationDN); if ((consecutiveFailures == null) || (outstandingFailures.get() > consecutiveFailures.get())) { consecutiveFailedBindsByDN.put(authenticationDN, new AtomicLong(outstandingFailures.get())); } } } final String ccp = m.getClientConnectionPolicy(); if (ccp != null) { AtomicLong l = clientConnectionPolicies.get(ccp); if (l == null) { l = new AtomicLong(0L); clientConnectionPolicies.put(ccp, l); } l.incrementAndGet(); } } else if ((m.getResultCode() != ResultCode.SASL_BIND_IN_PROGRESS) && (m.getResultCode() != ResultCode.REFERRAL)) { if (authenticationDN == null) { authenticationDN = getDNString(m.getDN()); } if (authenticationDN != null) { AtomicLong l = bindFailuresByDN.get(authenticationDN); if (l == null) { l = new AtomicLong(0L); bindFailuresByDN.put(authenticationDN, l); } l.incrementAndGet(); l = outstandingFailedBindDNs.get(authenticationDN); if (l == null) { l = new AtomicLong(0L); outstandingFailedBindDNs.put(authenticationDN, l); } l.incrementAndGet(); } String ipAddress = m.getRequesterIPAddress(); if (ipAddress == null) { final Long connectionID = m.getConnectionID(); if (connectionID != null) { ipAddress = ipAddressesByConnectionID.get(connectionID); } } if (ipAddress != null) { AtomicLong l = bindFailuresByIPAddress.get(ipAddress); if (l == null) { l = new AtomicLong(0L); bindFailuresByIPAddress.put(ipAddress, l); } l.incrementAndGet(); } } final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedBinds++; } updateAuthzCount(m.getAuthorizationDN()); } /** * Performs any necessary processing for a compare result message. * * @param m The log message to be processed. */ private void processCompareResult( @NotNull final CompareResultAccessLogMessage m) { numCompares++; updateCommonResult(m); updateResultCodeCount(m.getResultCode(), compareResultCodes); compareProcessingDuration += doubleValue(m.getProcessingTimeMillis(), compareProcessingTimes); final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedCompares++; } updateAuthzCount(m.getAlternateAuthorizationDN()); } /** * Performs any necessary processing for a delete result message. * * @param m The log message to be processed. */ private void processDeleteResult( @NotNull final DeleteResultAccessLogMessage m) { numDeletes++; updateCommonResult(m); updateResultCodeCount(m.getResultCode(), deleteResultCodes); deleteProcessingDuration += doubleValue(m.getProcessingTimeMillis(), deleteProcessingTimes); final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedDeletes++; } updateAuthzCount(m.getAlternateAuthorizationDN()); } /** * Performs any necessary processing for an extended result message. * * @param m The log message to be processed. */ private void processExtendedResult( @NotNull final ExtendedResultAccessLogMessage m) { numExtended++; updateCommonResult(m); final String id = m.getConnectionID() + "-" + m.getOperationID(); if (!processedRequests.remove(id)) { processExtendedRequestInternal(m); } updateResultCodeCount(m.getResultCode(), extendedResultCodes); extendedProcessingDuration += doubleValue(m.getProcessingTimeMillis(), extendedProcessingTimes); final String ccp = m.getClientConnectionPolicy(); if (ccp != null) { AtomicLong l = clientConnectionPolicies.get(ccp); if (l == null) { l = new AtomicLong(0L); clientConnectionPolicies.put(ccp, l); } l.incrementAndGet(); } final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedExtended++; } } /** * Performs any necessary processing for a modify result message. * * @param m The log message to be processed. */ private void processModifyResult( @NotNull final ModifyResultAccessLogMessage m) { numModifies++; updateCommonResult(m); updateResultCodeCount(m.getResultCode(), modifyResultCodes); modifyProcessingDuration += doubleValue(m.getProcessingTimeMillis(), modifyProcessingTimes); final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedModifies++; } updateAuthzCount(m.getAlternateAuthorizationDN()); } /** * Performs any necessary processing for a modify DN result message. * * @param m The log message to be processed. */ private void processModifyDNResult( @NotNull final ModifyDNResultAccessLogMessage m) { numModifyDNs++; updateCommonResult(m); updateResultCodeCount(m.getResultCode(), modifyDNResultCodes); modifyDNProcessingDuration += doubleValue(m.getProcessingTimeMillis(), modifyDNProcessingTimes); final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedModifyDNs++; } updateAuthzCount(m.getAlternateAuthorizationDN()); } /** * Performs any necessary processing for a search result message. * * @param m The log message to be processed. */ private void processSearchResult( @NotNull final SearchResultAccessLogMessage m) { numSearches++; updateCommonResult(m); final String id = m.getConnectionID() + "-" + m.getOperationID(); if (! processedRequests.remove(id)) { processSearchRequestInternal(m); } final ResultCode resultCode = m.getResultCode(); updateResultCodeCount(resultCode, searchResultCodes); searchProcessingDuration += doubleValue(m.getProcessingTimeMillis(), searchProcessingTimes); final String filterString = prepareFilter(m.getFilter()); final Long entryCount = m.getEntriesReturned(); if (entryCount != null) { AtomicLong l = searchEntryCounts.get(entryCount); if (l == null) { l = new AtomicLong(0L); searchEntryCounts.put(entryCount, l); } l.incrementAndGet(); final Map filterCountMap; switch (entryCount.intValue()) { case 0: filterCountMap = noEntryFilters; break; case 1: filterCountMap = oneEntryFilters; break; default: filterCountMap = multiEntryFilters; break; } if (filterString != null) { AtomicLong filterCount = filterCountMap.get(filterString); if (filterCount == null) { filterCount = new AtomicLong(0L); filterCountMap.put(filterString, filterCount); } filterCount.incrementAndGet(); } } final Boolean isUnindexed = m.getUnindexed(); if ((isUnindexed != null) && isUnindexed) { numUnindexedAttempts++; if (resultCode == ResultCode.SUCCESS) { numUnindexedSuccessful++; } else { numUnindexedFailed++; } if (filterString != null) { AtomicLong l = unindexedFilters.get(filterString); if (l == null) { l = new AtomicLong(0L); unindexedFilters.put(filterString, l); } l.incrementAndGet(); } } final Boolean uncachedDataAccessed = m.getUncachedDataAccessed(); if ((uncachedDataAccessed != null) && uncachedDataAccessed) { numUncachedSearches++; } updateAuthzCount(m.getAlternateAuthorizationDN()); final Double processingTimeMillis = m.getProcessingTimeMillis(); if ((processingTimeMillis != null) && (filterString != null)) { final long processingTimeMicros = Math.round(processingTimeMillis * 1_000.0); AtomicLong l = mostExpensiveFilters.get(filterString); if (l == null) { l = new AtomicLong(processingTimeMicros); mostExpensiveFilters.put(filterString, l); } else { final long previousProcessingTimeMicros = l.get(); if (processingTimeMicros > previousProcessingTimeMicros) { l.set(processingTimeMicros); } } } } /** * Updates a number of statistics that are common to all types of result log * messages. * * @param m The result log message to examine. */ private void updateCommonResult( @NotNull final OperationResultAccessLogMessage m) { // Handle the work queue wait time. totalWorkQueueWaitTime += doubleValue(m.getWorkQueueWaitTimeMillis(), workQueueWaitTimes); // Handle request and response control OIDs. for (final String oid : m.getRequestControlOIDs()) { numRequestControls++; updateCount(requestControlOIDs, oid); } for (final String oid : m.getResponseControlOIDs()) { numResponseControls++; updateCount(responseControlOIDs, oid); } // Handle used and missing privileges. for (final String privilegeName : m.getPreAuthorizationUsedPrivileges()) { updateCount(preAuthzPrivilegesUsed, privilegeName); } for (final String privilegeName : m.getUsedPrivileges()) { updateCount(privilegesUsed, privilegeName); } for (final String privilegeName : m.getMissingPrivileges()) { updateCount(privilegesMissing, privilegeName); } } /** * Updates the counter for the given key in the provided map. If the key does * not exist, it will be added to the map. * * @param m The map to be updated. * @param key The key for which to update the count. */ private static void updateCount(@NotNull final Map m, @NotNull final String key) { AtomicLong count = m.get(key); if (count == null) { count = new AtomicLong(0L); m.put(key, count); } count.incrementAndGet(); } /** * Updates the count for the provided result code in the given map. * * @param rc The result code for which to update the count. * @param m The map used to hold counts by result code. */ private static void updateResultCodeCount(@Nullable final ResultCode rc, @NotNull final HashMap m) { if (rc == null) { return; } AtomicLong l = m.get(rc); if (l == null) { l = new AtomicLong(0L); m.put(rc, l); } l.incrementAndGet(); } /** * Retrieves the double value for the provided {@code Double} object. * * @param d The {@code Double} object for which to retrieve the value. * @param m The processing time histogram map to be updated. * * @return The double value of the provided {@code Double} object if it was * non-{@code null}, or 0.0 if it was {@code null}. */ private static double doubleValue(@Nullable final Double d, @NotNull final HashMap m) { if (d == null) { return 0.0; } else { for (final Map.Entry e : m.entrySet()) { if (d <= e.getKey()) { e.getValue().incrementAndGet(); break; } } return d; } } /** * Updates the provided list with the most frequently-occurring elements in * the provided map, paired with the number of times each value occurred. * * @param The type of object used as the key for the * provided map. * @param countMap The map to be examined. It is expected that * the values of the map will be the count of * occurrences for the keys. * @param mostCommonElementList The list to which the values will be * updated. It must not be {@code null}, must * be empty, and must be updatable. * @param maxListSize The maximum number of items to add to the * provided list. It must be greater than * zero. * @param skippedWithSameCount A counter that will be incremented for each * map entry that is skipped with the same * count as a value that was not skipped. It * must not be {@code null} and must initially * be zero. * @param skippedWithLowerCount A counter that will be incremented for each * map entry that is skipped with a lower count * as the last value that was not skipped. It * must not be {@code null} and must initially * be zero. * * @return A list of the most frequently-occurring elements in the provided * map. */ @NotNull() private static List> getMostCommonElements( @NotNull final Map countMap, @NotNull final List> mostCommonElementList, final int maxListSize, @NotNull final AtomicLong skippedWithSameCount, @NotNull final AtomicLong skippedWithLowerCount) { final TreeMap> reverseMap = new TreeMap<>(new ReverseComparator()); for (final Map.Entry e : countMap.entrySet()) { final Long count = e.getValue().get(); List list = reverseMap.get(count); if (list == null) { list = new ArrayList<>(); reverseMap.put(count, list); } list.add(e.getKey()); } for (final Map.Entry> e : reverseMap.entrySet()) { final Long l = e.getKey(); int numNotSkipped = 0; for (final K k : e.getValue()) { if (mostCommonElementList.size() >= maxListSize) { if (numNotSkipped > 0) { skippedWithSameCount.incrementAndGet(); } else { skippedWithLowerCount.incrementAndGet(); } } else { numNotSkipped++; mostCommonElementList.add(new ObjectPair<>(k, l)); } } } return mostCommonElementList; } /** * Updates the count of alternate authorization identities for the provided * DN. * * @param authzDN The DN of the alternate authorization identity that was * used. It may be {@code null} if no alternate * authorization identity was used. */ private void updateAuthzCount(@Nullable final String authzDN) { if (authzDN == null) { return; } final String dnString = getDNString(authzDN); AtomicLong l = authzDNs.get(dnString); if (l == null) { l = new AtomicLong(0L); authzDNs.put(dnString, l); } } /** * Retrieves a string representation of the provided DN. It may either be * anonymized, using question marks in place of specific attribute values, or * it may be the actual string representation of the given DN. * * @param dn The DN for which to retrieve the string representation. * * @return A string representation of the provided DN, or {@code null} if the * given DN was {@code null}. */ @Nullable() private String getDNString(@Nullable final String dn) { if (dn == null) { return null; } final DN parsedDN; try { parsedDN = new DN(dn); } catch (final Exception e) { Debug.debugException(e); return dn.toLowerCase(); } if (parsedDN.isNullDN()) { return "{Null DN}"; } if (doNotAnonymize.isPresent()) { return parsedDN.toNormalizedString(); } final StringBuilder buffer = new StringBuilder(); final RDN[] rdns = parsedDN.getRDNs(); for (int i=0; i < rdns.length; i++) { if (i > 0) { buffer.append(','); } final RDN rdn = rdns[i]; final String[] attributeNames = rdn.getAttributeNames(); for (int j=0; j < attributeNames.length; j++) { if (j > 0) { buffer.append('+'); } buffer.append(attributeNames[j].toLowerCase()); buffer.append("=?"); } } return buffer.toString(); } /** * Retrieves a prepared string representation of the provided search filter. * It may potentially be de-anonymized to include specific values. * * @param filterString The string representation of the filter to prepare. * It may be {@code null} if the log message does not * have a filter. * * @return A string representation of the provided filter (which may or may * not be anonymized), or {@code null} if the provided filter is * {@code null} or cannot be prepared. */ @Nullable() private String prepareFilter(@Nullable final String filterString) { if (filterString == null) { return null; } if (doNotAnonymize.isPresent()) { return filterString.toLowerCase(); } try { return new GenericFilter(Filter.create(filterString)).toString(). toLowerCase(); } catch (final Exception e) { Debug.debugException(e); return null; } } /** * Writes a breakdown of the processing times for a specified type of * operation. * * @param t The name of the operation type. * @param n The total number of operations of the specified type that were * processed by the server. * @param m The map of operation counts by processing time bucket. */ private void printProcessingTimeHistogram(@NotNull final String t, final long n, @NotNull final LinkedHashMap m) { printHistogram("Count of " + t + " operations by processing time:", n, m); } /** * Writes a breakdown of the processing times for a specified type of * operation. * * @param h The header to display at the beginning of the histogram. * @param n The total number of operations that were processed by the * server. * @param m The map of operation counts by processing time bucket. */ private void printHistogram(@NotNull final String h, final long n, @NotNull final LinkedHashMap m) { if (n <= 0) { return; } out(); out(h); long lowerBound = 0; long accumulatedCount = 0; final Iterator> i = m.entrySet().iterator(); while (i.hasNext()) { final Map.Entry e = i.next(); final long upperBound = e.getKey(); final long count = e.getValue().get(); final double categoryPercent = 100.0 * count / n; accumulatedCount += count; final double accumulatedPercent = 100.0 * accumulatedCount / n; if (i.hasNext()) { final String lowerBoundString; if (lowerBound == 0L) { lowerBoundString = "0 milliseconds"; } else { final long lowerBoundNanos = lowerBound * 1_000_000L; lowerBoundString = DurationArgument.nanosToDuration(lowerBoundNanos); } final long upperBoundNanos = upperBound * 1_000_000L; final String upperBoundString = DurationArgument.nanosToDuration(upperBoundNanos); out("Between ", lowerBoundString, " and ", upperBoundString, ": ", count, " (", decimalFormat.format(categoryPercent), "%, ", decimalFormat.format(accumulatedPercent), "% accumulated)"); lowerBound = upperBound; } else { final long lowerBoundNanos = lowerBound * 1_000_000L; final String lowerBoundString = DurationArgument.nanosToDuration(lowerBoundNanos); out("Greater than ", lowerBoundString, ": ", count, " (", decimalFormat.format(categoryPercent), "%, ", decimalFormat.format(accumulatedPercent), "% accumulated)"); } } } /** * Optionally prints information about the number and percent of operations of * the specified type that involved access to uncached data. * * @param operationType The type of operation. * @param numUncached The number of operations of the specified type that * involved access to uncached data. * @param numTotal The total number of operations of the specified * type. */ private void printUncached(@NotNull final String operationType, final long numUncached, final long numTotal) { if (numUncached == 0) { return; } out(operationType, ": ", numUncached, " (", decimalFormat.format(100.0 * numUncached / numTotal), "%)"); } /** * Prints data from the provided map of counts. * * @param countMap The map containing the data to print. * @param heading The heading to display before printing the contents * of the map. * @param singularItem The name to use for a single item represented by the * key of the given map. * @param pluralItem The name to use for zero or multiple items * represented by the key of the given map. */ private void printCounts(@Nullable final Map countMap, @NotNull final String heading, @NotNull final String singularItem, @NotNull final String pluralItem) { if ((countMap == null) || countMap.isEmpty()) { return; } long totalCount = 0L; for (final AtomicLong l : countMap.values()) { totalCount += l.get(); } out(); out(heading); int displayCount = reportCount.getValue(); if (displayCount <= 0L) { displayCount = Integer.MAX_VALUE; } final List> countList = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(countMap, countList, displayCount, skippedWithSameCount, skippedWithLowerCount); long count = -1L; for (final ObjectPair p : countList) { count = p.getSecond(); if (totalCount > 0L) { final double percent = 100.0 * count / totalCount; out(p.getFirst(), ": ", count, " (", decimalFormat.format(percent), ")"); } else { out(p.getFirst(), ": ", count); } } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional " + getSingularOrPlural(skippedWithSameCount.get(), singularItem, pluralItem) + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional " + getSingularOrPlural(skippedWithLowerCount.get(), singularItem, pluralItem) + " with a count that is less than " + count + " }"); } } /** * Prints data from the provided map of counts. * * @param countMap The map containing the data to print. * @param operationType The type of operation represented by the keys of * the map. */ private void printResultCodeCounts( @Nullable final Map countMap, @NotNull final String operationType) { if ((countMap == null) || countMap.isEmpty()) { return; } long totalCount = 0L; for (final AtomicLong l : countMap.values()) { totalCount += l.get(); } out(); out("Most common " + operationType + " operation result codes:"); int displayCount = reportCount.getValue(); if (displayCount <= 0L) { displayCount = Integer.MAX_VALUE; } final List> resultCodeList = new ArrayList<>(); final AtomicLong skippedWithSameCount = new AtomicLong(0L); final AtomicLong skippedWithLowerCount = new AtomicLong(0L); getMostCommonElements(countMap, resultCodeList, displayCount, skippedWithSameCount, skippedWithLowerCount); long count = -1L; for (final ObjectPair p : resultCodeList) { count = p.getSecond(); if (totalCount > 0L) { final double percent = 100.0 * count / totalCount; out(p.getFirst().getName(), " (", p.getFirst().intValue(), "): ", count, " (", decimalFormat.format(percent), ")"); } else { out(p.getFirst(), ": ", count); } } if (skippedWithSameCount.get() > 0L) { out("{ Skipped " + skippedWithSameCount.get() + " additional result " + getSingularOrPlural(skippedWithSameCount.get(), "code", "codes") + " with a count of " + count + " }"); } if (skippedWithLowerCount.get() > 0L) { out("{ Skipped " + skippedWithLowerCount.get() + " additional result " + getSingularOrPlural(skippedWithLowerCount.get(), "code", "codes") + " with a count that is less than " + count + " }"); } } /** * Retrieves the appropriate singular or plural form based on the given * value. * * @param count The count that will be used to determine whether to * retrieve the singular or plural form. * @param singular The singular form for the value to return. * @param plural The plural form for the value to return. * * @return The singular form if the count is 1, or the plural form if the * count is any other value. */ @NotNull() private String getSingularOrPlural(final long count, @NotNull final String singular, @NotNull final String plural) { if (count == 1L) { return singular; } else { return plural; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy