CentralLogFormatter.java
/*
** Module : CentralLogFormatter.java
** Abstract : Log formatter for CentralLogger.
**
** Copyright (c) 2023-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------Description-----------------------------
** 001 GBB 20230328 Initial setup
** 002 GBB 20230613 convertToLevel moved to LoggingUtils.
** 003 GBB 20230623 Performance improvements
** 004 GBB 20230825 SecurityManager context & session methods calls updated.
** 005 TJD 20240117 Do not modify needToInferCaller directly
** 006 GBB 20240729 Save log context in a new object and print it later with the message.
** 007 GBB 20250403 Checks for sessionId being null and fwdUser being an empty string.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General 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 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.util.logging;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.security.SecurityManager;
import java.io.*;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import java.util.logging.Formatter;
import java.util.regex.*;
/**
* Extends the java formatter base class used by {@link FileHandler}. Exposes portions of the formatting
* logic to be reused by other formatters or directly by the CentralLogger. Provides a parse method to
* convert back formatted text into CentralLogRecord.
*/
public class CentralLogFormatter
extends Formatter
{
/** The divider between different parts of the log message. */
static final String LOG_PARTS_DIVIDER = " | ";
/** The datetime formatter used for each log record. Similar to the one used in 4GL LOG-MANAGER. */
private static final SimpleDateFormat DATETIME_FORMATTER =
new SimpleDateFormat("YY/MM/dd HH:mm:ss.SSSXX");
/** The line separator for the operating system. */
private static final String LINE_SEPARATOR = System.lineSeparator();
/**
* Get a string which uniquely describes the current context. Consists of multiple parts:
* - the name of the logger mode,
* - (for clients with initialized native lib) process id,
* - thread name,
* - (when security manager has context initialized) session, thread and user id.
*
* @param sessionId
* The remote server session ID.
* @param userId
* The remote server user ID.
* @param excludeSMContext
* Flag to prevent deadlock on SecurityManager, ref #5703#note-77.
*
* @return Description of the context.
*/
protected static ContextLogRecord.LogContext describeContext(Integer sessionId, String userId, boolean excludeSMContext)
{
ContextLogRecord.LogContext logContext = new ContextLogRecord.LogContext();
if (ClientCore.isNativeLayerInitialized())
{
logContext.setPid(ClientCore.getPid());
}
logContext.setThreadName(Thread.currentThread().getName());
if (sessionId != null || userId != null)
{
logContext.setSessionId(sessionId == null || sessionId == 0 ?
null :
LoggingUtil.addLeadingZeros(sessionId, 8));
logContext.setFwdUser(userId);
}
else if (!excludeSMContext)
{
SecurityManager sm = SecurityManager.getInstance();
if (sm != null && SecurityManager.initialized() && sm.contextSm.hasContext())
{
sessionId = sm.sessionSm.getSessionId();
logContext.setSessionId(sessionId == null || sessionId == 0 ?
null :
LoggingUtil.addLeadingZeros(sessionId, 8));
logContext.setThreadId(LoggingUtil.addLeadingZeros(sm.getThreadId(), 8));
logContext.setFwdUser(sm.getUserId());
}
}
return logContext;
}
/**
* Convert back formatted text into CentralLogRecord.
*
* @param logLine
* The formatted log.
*
* @return CentralLogRecord parsed from the provided text.
*/
static CentralLogRecord parse(String logLine)
{
String[] logRecordParts = logLine.split(Pattern.quote(LOG_PARTS_DIVIDER));
if (logRecordParts.length < 5)
{
return null;
}
CentralLogRecord clr = new CentralLogRecord();
try
{
clr.setMillis(DATETIME_FORMATTER.parse(logRecordParts[0].trim()).getTime());
}
catch (Throwable ignored)
{
// ignore
}
clr.setLevel(LoggingUtil.convertToLevel(logRecordParts[1].trim()).orElse(null));
// concatenates original logger name and source class, method
clr.setLoggerName(logRecordParts[2].trim());
// concatenates original context and the actual message
String msg = "[" + logRecordParts[3].trim() + "] " + logRecordParts[4];
if (logRecordParts.length > 5)
{
String[] msgSegments = Arrays.copyOfRange(logRecordParts, 5, logRecordParts.length - 1);
msg += String.join("", msgSegments);
}
clr.setMessage(msg);
return clr;
}
/**
* Dump all details of the throwable into the given string buffer.
* Includes exception type, message, complete stack trace, and chained
* throwables, if any.
*
* @param sb
* The string builder the throwable to be appended to.
* @param original
* The throwable to dump.
*/
static void dumpThrowable(StringBuilder sb, Throwable original)
{
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
original.printStackTrace(printWriter);
printWriter.flush();
sb.append(writer.toString());
}
/**
* Appends the log message to the string builder.
*
* @param sb
* The string builder the message to be appended to.
* @param record
* The log record with the message or the message format and parameters.
*/
static void dumpMessage(StringBuilder sb, LogRecord record)
{
if (!CentralLogger.WRITE_TO_SLF4J_API && record instanceof ContextLogRecord)
{
ContextLogRecord.LogContext context = ((ContextLogRecord) record).getLogContext();
if (context != null)
{
boolean isContextAdded = false;
if (context.getPid() != null)
{
sb.append("PID:").append(context.getPid());
isContextAdded = true;
}
if (context.getThreadName() != null)
{
sb.append(isContextAdded ? ", " : "").append("ThreadName:").append(context.getThreadName());
isContextAdded = true;
}
if (context.getSessionId() != null)
{
sb.append(isContextAdded ? ", " : "").append("Session:").append(context.getSessionId());
isContextAdded = true;
}
if (context.getThreadId() != null)
{
sb.append(isContextAdded ? ", " : "").append("ThreadId:").append(context.getThreadId());
isContextAdded = true;
}
if (context.getFwdUser() != null && !context.getFwdUser().equals(""))
{
sb.append(isContextAdded ? ", " : "").append("User:").append(context.getFwdUser());
}
sb.append(LOG_PARTS_DIVIDER);
}
}
Object[] params = record.getParameters();
if (params == null || params.length == 0)
{
sb.append(record.getMessage());
return;
}
sb.append(String.format(record.getMessage(), params));
}
/**
* Transforms the log record to string. A divider separates the different parts:
* - datetime,
* - log level,
* - logger name,
* - (optional) source class name, source method name,
* - message (no divider after it),
* - (optional) throwable.
*
* @param record
* The log record.
*
* @return The string representation of the log record.
*/
@Override
public String format(LogRecord record)
{
StringBuilder sb = new StringBuilder(DATETIME_FORMATTER.format(new Date(record.getMillis())));
sb.append(LOG_PARTS_DIVIDER);
sb.append(LoggingUtil.addLeadingSymbols(record.getLevel().toString(), 7, ' '));
sb.append(LOG_PARTS_DIVIDER);
sb.append(record.getLoggerName());
String sourceClassName = record.getSourceClassName();
if (sourceClassName != null)
{
sb.append(" [").append(sourceClassName);
String sourceMethodName = record.getSourceMethodName();
if (sourceMethodName != null && !sourceMethodName.trim().isEmpty())
{
sb.append(".").append(sourceMethodName).append("()");
}
sb.append("]");
}
sb.append(LOG_PARTS_DIVIDER);
dumpMessage(sb, record);
sb.append(LINE_SEPARATOR);
if (record.getThrown() != null)
{
dumpThrowable(sb, record.getThrown());
}
return sb.toString();
}
}