SQLStatementLoggerConfiguration.java

/*
** Module   : SQLStatementLoggerConfiguration.java
** Abstract : Store configuration for SQLStatementLogger
**
** Copyright (c) 2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 RAA 20230608 First revision with basic usage.
**     RAA 20230609 Moved Configuration logic in main class.
** 002 RAA 20230615 Added ALLOW_SENSIBLE flag.
**     RAA 20230615 Replaced SENSIBLE keyword with SENSITIVE.
** 003 RAA 20230724 Replaced LOGGING_ENABLED with an enum, LoggingType.
**     RAA 20231011 Allowed configuration of sensitive data for _temp database.
** 004 ICP 20240618 Added logging to a h2 database feature.
**     ICP 20240813 Instead of using a FileHandler, the CentralLoggerFile is used now
**                  to log the SQL statements.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist.orm;

import static com.goldencode.p2j.util.logging.LoggingUtil.resolveLogFile;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.logging.*;

import com.goldencode.p2j.directory.DirectoryService;
import com.goldencode.p2j.main.ClientCore;
import com.goldencode.p2j.util.Utils;
import com.goldencode.p2j.util.logging.*;

/**
 * Class responsible with storing the configuration for {@link SQLStatementLogger}.
 */
public final class SQLStatementLoggerConfiguration
{
   /** Map that includes a configuration for each given database. */
   private static final Map<String, SQLStatementLoggerConfiguration> configs = new HashMap<>();
   
   /** Logger. */
   private static final CentralLogger LOG = CentralLogger.get(SQLStatementLoggerConfiguration.class);

   /** The log formatter. */
   private static final Formatter LOG_FORMATTER = new CentralLogFormatter();
   
   /** Global directory path, used if a local one does not exist. */
   private static final String GLOBAL_DIR_PATH = "persistence/sql-logging";
   
   /** The type of logging intended. */
   private final LoggingType loggingType;

   /** The file logger, responsible for writing logs to files. */
   private static CentralLoggerFile fileLogger;

   /** Is stack tracing of SQL queries intended? If {@code LOGGING_ENABLED} is {@code false}, then this
    *  flag won't matter. */
   private final boolean STACK_TRACING;
   
   /** Flag for a minimum time threshold. SQL queries that execute in less time than this threshold 
    *  will not be logged. The default value is {@code 0.0} milliseconds.*/
   private final double MIN_TIME_THRESHOLD;
   
   /** Flag for ignoring the SQL queries that belong to the {@code meta} database. */
   private final boolean IGNORE_META_LOGGING;
   
   /** Flag that marks whether potential sensitive data is allowed to be logged. */
   private final boolean ALLOW_SENSITIVE;
   
   /** The file that will receive the logging output. */
   private final String OUTPUT_FILE;
   
   /** The file name for the h2 database that will receive the logging output . */
   private final String OUTPUT_DB;
   
   /** The user for the h2 database that will receive the logging output . */
   private final String USER_DB;
   
   /** The password for the h2 database that will receive the logging output . */
   private final String PASS_DB;
   
   /** The helper object to facilitate logging to H2 database*/
   private volatile SQLLoggingDatabaseHelper dbHelper;
   
   /**
    * Constructor of a configuration.
    * 
    * First, it checks if logging is enabled.
    * If this condition is met, it then proceeds with computing the other details (such as profiling,
    * stack tracing, etc.).
    * Lastly, it creates the corresponding {@code FileHandler}.
    * 
    * @param   databaseName
    *          The name of the database in use.
    */
   private SQLStatementLoggerConfiguration(String databaseName)
   {
      boolean enabled = false;
      boolean profilingEnabled = false;
      boolean outputDbEnabled = false;
      boolean stackTracing = false;
      double minTime = 0;
      boolean ignoreMetaLogging = false;
      boolean hasLocalConfiguration = false;
      boolean allowSensitive = false;
      String outputFile = null;
      String outputDb = null;
      String userDb = null;
      String passDb = null;
      String defaultOutputFile = "sql_logging_%db_" + 
                                 LoggingUtil.SORTABLE_DATETIME_FORMAT + 
                                 "_%uos_%pid_%as.log";
      String defaultOutputDb = "sql_logging_%db_" + 
                               LoggingUtil.SORTABLE_DATETIME_FORMAT + 
                               "_%uos_%pid_%as";
      String defaultUserDb = "";
      String defaultPassDb = "";
      String directoryPath = null;
      String localDirectoryPath = "database/" + databaseName + "/sql-logging";
      DirectoryService ds = DirectoryService.getInstance();
      
      if (ds != null)
      {
         try
         {
            if (!ds.bind())
            {
               LOG.warning("SQL logging is not enabled");
            }
            
            String isLocallyEnabled = Utils.getDirectoryNodeString(ds, 
                                                                   localDirectoryPath + "/enabled", 
                                                                   "NULL", 
                                                                   false);
            switch (isLocallyEnabled.toUpperCase())
            {
               case "NULL":
                  String globalDirectoryPath = GLOBAL_DIR_PATH + "/enabled";
                  enabled = Boolean.parseBoolean(Utils.getDirectoryNodeString(ds, 
                                                                              globalDirectoryPath, 
                                                                              "FALSE", 
                                                                              false));
                  break;
               case "TRUE":
                  enabled = true;
                  hasLocalConfiguration = true;
                  break;
               case "FALSE":
                  enabled = false;
                  hasLocalConfiguration = true;
                  break;
               default:
                  throw new RuntimeException("Invalid value for attribute: enabled");
            }
            
            if (enabled)
            {
               directoryPath = hasLocalConfiguration ? localDirectoryPath : GLOBAL_DIR_PATH;
               profilingEnabled = Utils.getDirectoryNodeBoolean(ds, 
                                                                directoryPath + "/profiling", 
                                                                false, 
                                                                false);
               if (profilingEnabled)
               {
                  minTime = Utils.getDirectoryNodeDouble(ds, 
                                                         directoryPath + "/min-time-threshold", 
                                                         0, 
                                                         false);
               }
               
               outputDbEnabled = Utils.getDirectoryNodeBoolean(ds, 
                                                               directoryPath + "/output-to-db/enabled", 
                                                               false, 
                                                               false);
               if (outputDbEnabled)
               {
                  outputDb = Utils.getDirectoryNodeString(ds, 
                                                          directoryPath + "/output-to-db/db-name", 
                                                          defaultOutputDb,
                                                          false);
                  userDb = Utils.getDirectoryNodeString(ds, 
                                                        directoryPath + "/output-to-db/db-user", 
                                                        defaultUserDb,
                                                        false);
                  passDb = Utils.getDirectoryNodeString(ds, 
                                                        directoryPath + "/output-to-db/db-pass", 
                                                        defaultPassDb,
                                                        false);
               }
               stackTracing = Utils.getDirectoryNodeBoolean(ds, 
                                                            directoryPath + "/stack-tracing", 
                                                            false, 
                                                            false);
               ignoreMetaLogging = Utils.getDirectoryNodeBoolean(ds, 
                                                                 directoryPath + "/ignore-meta-logging",
                                                                 false, 
                                                                 false);
               outputFile = Utils.getDirectoryNodeString(ds, 
                                                         directoryPath + "/output-to-file", 
                                                         defaultOutputFile,
                                                         false);
               allowSensitive = Utils.getDirectoryNodeBoolean(ds, 
                                                              directoryPath + "/allow-sensitive", 
                                                              false,
                                                              false);
            }
         }
         finally
         {
            ds.unbind();
         }
      }
      
      MIN_TIME_THRESHOLD = minTime;
      STACK_TRACING = stackTracing;
      IGNORE_META_LOGGING = ignoreMetaLogging;
      OUTPUT_FILE = outputFile;
      OUTPUT_DB = outputDb;
      USER_DB = userDb;
      PASS_DB = passDb;
      ALLOW_SENSITIVE = allowSensitive;
      
      if (!enabled)
      {
         loggingType = LoggingType.NONE;
         return;
      }
      
      loggingType = profilingEnabled ? LoggingType.PROFILING : LoggingType.LOGGING;
      
      Supplier<Long> processIdSupplier = () -> {
         try 
         {
            System.loadLibrary("p2j");
            return ClientCore.getPid();
         }
         catch (Throwable t)
         {
            LOG.log(Level.FINE, "", t);
            return 0L;
         }
      };
      
      String formattedFilePath = resolveLogFile(OUTPUT_FILE,
                                                processIdSupplier,
                                                "server",
                                                databaseName,
                                                true);

      fileLogger = CentralLoggerFile.initialize("sql_" + databaseName + "_logger", formattedFilePath, 0, 0);

      if (outputDbEnabled)
      {
         String formattedDbPath = resolveLogFile(OUTPUT_DB,
                                                 processIdSupplier,
                                                 "server",
                                                 databaseName,
                                                 true);
         dbHelper = SQLLoggingDatabaseHelper.getDbHelper(databaseName, formattedDbPath, USER_DB, PASS_DB);
      }
   }

   /**
    * Get the logging configuration of a specific database.
    * 
    * @param   databaseName
    *          The name of the database in use.
    *          
    * @return  SQLStatementLoggerConfiguration
    *          If it exists, an already created configuration for that database, or a new one otherwise.
    */
   public static SQLStatementLoggerConfiguration getConfiguration(String databaseName)
   {
      SQLStatementLoggerConfiguration config = configs.get(databaseName);
      if (config != null)
      {
         return config;
      }
      
      config = new SQLStatementLoggerConfiguration(databaseName);
      configs.put(databaseName, config);
      return config;
   }
   
   /**
    * Getter for the {@code loggingType} field.
    * 
    * @return  LoggingType
    *          The type of logger.
    */
   public LoggingType getLoggingType()
   {
      return loggingType;
   }
   
   /**
    * Getter for the {@code STACK_TRACING} field.
    * 
    * @return  boolean
    *          The value of {@code STACK_TRACING}.
    */
   public boolean isStackTracingEnabled()
   {
      return STACK_TRACING;
   }
   
   /**
    * Getter for the {@code IGNORE_META_LOGGING} field.
    * 
    * @return  boolean
    *          The value of {@code IGNORE_META_LOGGING}.
    */
   public boolean isIgnoreMetaLogging()
   {
      return IGNORE_META_LOGGING;
   }
   
   /**
    * Getter for the {@code ALLOW_SENSITIVE} field.
    * 
    * @return  boolean
    *          The value of {@code ALLOW_SENSITIVE}.
    */
   public boolean isAllowSensitive()
   {
      return ALLOW_SENSITIVE;
   }
   
   /**
    * Getter for the {@code MIN_TIME_THRESHOLD} field.
    * 
    * @return  double
    *          The value of {@code MIN_TIME_THRESHOLD}.
    */
   public double getMinTimeThreshold()
   {
      return MIN_TIME_THRESHOLD;
   }
   
   /**
    * Getter for the {@code dbHelper} field.
    * 
    * @return   SQLLoggingDatabaseHelper
    *           The value of {@code dbHelper}.
    */
   public SQLLoggingDatabaseHelper getDbHelper()
   {
      return dbHelper;
   }
   
   /**
    * Enum denoting what should happen with a statement: should it just be executed or the intention is to
    * log things about it?
    */
   public enum LoggingType
   {
      /** Statement should be executed without logging anything. */
      NONE,
      
      /** Logging is enabled. */
      LOGGING,
      
      /** More than just logging, the statement should also be profiled. */
      PROFILING
   }

   /**
    * Getter for the {@code fileLogger} field.
    *
    * @return  CentralLoggerFile
    *          The value of {@code fileLogger}.
    */
   public CentralLoggerFile getFileLogger()
   {
      return fileLogger;
   }
}