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

com.sshtools.slf4jtty.TtyLogger Maven / Gradle / Ivy

package com.sshtools.slf4jtty;

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.jline.style.StyleExpression;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.slf4j.Logger;
import org.slf4j.Marker;
import org.slf4j.event.Level;
import org.slf4j.event.LoggingEvent;
import org.slf4j.helpers.LegacyAbstractLogger;
import org.slf4j.helpers.NormalizedParameters;
import org.slf4j.spi.LocationAwareLogger;

/**
 * 

* Simple implementation of {@link Logger} that sends all enabled log messages, * for all defined loggers, to the console ({@code System.err}), enabling colourised * and styled output using ANSI escape sequences. *

* It behaves similarly to "SimpleLogger", being originally based on it, but * all configuration is provided by INI files instead of property files, and this * is focused more on terminal output rather than log files. *

* It is also more flexible in the format, size and position of log fields. *

* TtyLogger and associated classes are based on SimpleLogger implementation to varying degrees. */ public class TtyLogger extends LegacyAbstractLogger { private static final long serialVersionUID = -632788891211436180L; private static final long START_TIME = System.currentTimeMillis(); protected static final int LOG_LEVEL_TRACE = LocationAwareLogger.TRACE_INT; protected static final int LOG_LEVEL_DEBUG = LocationAwareLogger.DEBUG_INT; protected static final int LOG_LEVEL_INFO = LocationAwareLogger.INFO_INT; protected static final int LOG_LEVEL_WARN = LocationAwareLogger.WARN_INT; protected static final int LOG_LEVEL_ERROR = LocationAwareLogger.ERROR_INT; static char SP = ' '; static final String TID_PREFIX = "tid="; // The OFF level can only be used in configuration files to disable logging. // It has // no printing method associated with it in o.s.Logger interface. protected static final int LOG_LEVEL_OFF = LOG_LEVEL_ERROR + 10; private static boolean INITIALIZED = false; static final TtyLoggerConfiguration CONFIG_PARAMS = new TtyLoggerConfiguration(); static void lazyInit() { if (INITIALIZED) { return; } INITIALIZED = true; init(); } // external software might be invoking this method directly. Do not rename // or change its semantics. static void init() { CONFIG_PARAMS.init(); } /** The current log level */ protected int currentLogLevel = LOG_LEVEL_INFO; /** The short name of this simple log instance */ private transient String shortLogName = null; private static ThreadLocal reentrant = new ThreadLocal<>(); private int lastWidth; private final Map fieldWidths = new HashMap<>(); /** * Package access allows only {@link TtyLoggerFactory} to instantiate * SimpleLogger instances. */ TtyLogger(String name) { this.name = name; String levelString = recursivelyComputeLevelString(); if (levelString != null) { this.currentLogLevel = TtyLoggerConfiguration.stringToLevel(levelString); } else { this.currentLogLevel = CONFIG_PARAMS.defaultLogLevel; } } String recursivelyComputeLevelString() { String tempName = name; String levelString = null; int indexOfLastDot = tempName.length(); while ((levelString == null) && (indexOfLastDot > -1)) { tempName = tempName.substring(0, indexOfLastDot); // TODO separate configuration file (without schema) for logger configuration //levelString = CONFIG_PARAMS.getStringProperty(TtyLogger.LOG_KEY_PREFIX + tempName, null); indexOfLastDot = String.valueOf(tempName).lastIndexOf("."); } return levelString; } /** * To avoid intermingling of log messages and associated stack traces, the two * operations are done in a synchronized block. * * @param buf * @param t */ void write(StringBuilder buf, Throwable t) { PrintStream targetStream = CONFIG_PARAMS.outputChoice.getTargetPrintStream(); synchronized (CONFIG_PARAMS) { targetStream.println(buf.toString()); writeThrowable(t, targetStream); targetStream.flush(); } } protected void writeThrowable(Throwable t, PrintStream targetStream) { if (t != null) { /* TODO configurable exception printing colors */ Throwable nex = t; int indent = 0; AttributedStringBuilder report = new AttributedStringBuilder(); while(nex != null) { if(indent > 0) { report.append(String.format("%" + ( 8 + ((indent - 1 )* 2) ) + "s", "")); report.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.RED).bold()); report.append(nex.getClass().getName() + ": " + nex.getMessage() == null ? "No message." : nex.getMessage()); report.style(AttributedStyle.DEFAULT); report.append(System.lineSeparator()); } for(var el : nex.getStackTrace()) { report.append(String.format("%" + ( 8 + (indent * 2) ) + "s", "")); report.append("at "); if(el.getModuleName() != null) { report.append(el.getModuleName()); report.append('/'); } report.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); report.append(el.getClassName() + "." + el.getMethodName()); report.style(AttributedStyle.DEFAULT); if(el.getFileName() != null) { report.append('('); report.append(el.getFileName()); if(el.getLineNumber() > -1) { report.append(':'); report.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); report.append(String.valueOf(el.getLineNumber())); report.style(AttributedStyle.DEFAULT); report.append(')'); } } report.append(System.lineSeparator()); } indent++; nex = nex.getCause(); } targetStream.print(report.toAttributedString().toAnsi(terminal())); } } private String getFormattedDate() { Date now = new Date(); String dateText; synchronized (CONFIG_PARAMS.dateFormatter) { dateText = CONFIG_PARAMS.dateFormatter.format(now); } return dateText; } private String computeShortName() { return name.substring(name.lastIndexOf(".") + 1); } /** * Is the given log level currently enabled? * * @param logLevel is this level enabled? * @return whether the logger is enabled for the given level */ protected boolean isLevelEnabled(int logLevel) { // log level are numerically ordered so can use simple numeric // comparison return (logLevel >= currentLogLevel); } /** Are {@code trace} messages currently enabled? */ public boolean isTraceEnabled() { return isLevelEnabled(LOG_LEVEL_TRACE); } /** Are {@code debug} messages currently enabled? */ public boolean isDebugEnabled() { return isLevelEnabled(LOG_LEVEL_DEBUG); } /** Are {@code info} messages currently enabled? */ public boolean isInfoEnabled() { return isLevelEnabled(LOG_LEVEL_INFO); } /** Are {@code warn} messages currently enabled? */ public boolean isWarnEnabled() { return isLevelEnabled(LOG_LEVEL_WARN); } /** Are {@code error} messages currently enabled? */ public boolean isErrorEnabled() { return isLevelEnabled(LOG_LEVEL_ERROR); } /** * SimpleLogger's implementation of * {@link org.slf4j.helpers.AbstractLogger#handleNormalizedLoggingCall(Level, Marker, String, Object[], Throwable) AbstractLogger#handleNormalizedLoggingCall} * } * * @param level the SLF4J level for this event * @param marker The marker to be used for this event, may be null. * @param messagePattern The message pattern which will be parsed and formatted * @param arguments the array of arguments to be formatted, may be null * @param throwable The exception whose stack trace should be logged, may be null */ @Override protected void handleNormalizedLoggingCall(Level level, Marker marker, String messagePattern, Object[] arguments, Throwable throwable) { /* JLine uses SLF4J for logging, which we may trigger by using it to style our text! Avoid * re-entering. */ Boolean reentered = reentrant.get(); if(Boolean.TRUE.equals(reentered)) { return; } try { reentrant.set(true); List markers = null; if (marker != null) { markers = new ArrayList<>(); markers.add(marker); } innerHandleNormalizedLoggingCall(level, markers, messagePattern, arguments, throwable); } finally { reentrant.remove(); } } private void innerHandleNormalizedLoggingCall(Level level, List markers, String messagePattern, Object[] arguments, Throwable t) { StringBuilder buf = new StringBuilder(32); synchronized(fieldWidths) { int width = getWidth(); if(this.lastWidth != width) { this.lastWidth = width; fieldWidths.clear(); int total = width - (Math.max(0, CONFIG_PARAMS.layout.size() - 1) * CONFIG_PARAMS.gap);; int available = total; int autoFields = 0; /* First pass that sets initial size of fixed size fields, and calculates * remaining space for any auto fields */ for(String field : CONFIG_PARAMS.layout) { int fieldWidth = CONFIG_PARAMS.fieldWidth.get(field); fieldWidths.put(field, fieldWidth); if(fieldWidth > 0) { available -= fieldWidth; if(available < 0) available = 0; } else autoFields++; } /* Give each auto field a portion of the available space (if any). */ int autoFieldSize = autoFields == 0 ? 0 : (int)((float)Math.max(autoFields, available) / (float)autoFields); for(Map.Entry en : fieldWidths.entrySet()) { if(en.getValue() == 0) { en.setValue(autoFieldSize); } } /* If the total width of the row exceeds the available width, remove one character * from each field until all fields will fit */ int totalWidth = fieldWidths.values().stream().reduce(0, Integer::sum); int overflow = totalWidth - width; Iterator> fieldIt = null; for(int i = 0 ; i < overflow; i++) { if(fieldIt == null || !fieldIt.hasNext()) { fieldIt = fieldWidths.entrySet().iterator(); } Map.Entry field = fieldIt.next(); field.setValue(Math.max(1, field.getValue() - 1)); } System.out.println("Col widths: " + String.join(", ", fieldWidths.values().stream().map(i -> String.valueOf(i)).toList()) + " in " + total + " (" + width + ")"); System.out.println("0123456789012345678901234567890123456789012345678901234567890123456789"); } } String defaultStyle = null; if(CONFIG_PARAMS.styleAsLevel) { defaultStyle = CONFIG_PARAMS.levelStyles.get(level); } AtomicInteger fieldIdx = new AtomicInteger(); for (String field : CONFIG_PARAMS.layout) { StringBuilder sdbuf = new StringBuilder(); if (field.equals("date-time")) { if (CONFIG_PARAMS.dateFormatter != null) { appendField(defaultStyle, sdbuf, field, getFormattedDate(), fieldIdx, fieldWidths.get(field)); } else { appendField(defaultStyle, sdbuf, field, String.valueOf(System.currentTimeMillis() - START_TIME), fieldIdx, fieldWidths.get(field)); } } else if (field.equals("thread-name")) { appendField(defaultStyle, sdbuf, field, Thread.currentThread().getName(), fieldIdx, fieldWidths.get(field)); } else if (field.equals("thread-id")) { appendField(defaultStyle, sdbuf, field, String.valueOf(Thread.currentThread().getId()), fieldIdx, fieldWidths.get(field)); } else if (field.equals("level")) { String levelStyle = CONFIG_PARAMS.levelStyles.get(level); String levelText = CONFIG_PARAMS.levelText.get(level); appendField(levelStyle, sdbuf, field, levelText, fieldIdx, fieldWidths.get(field)); } else if (field.equals("short-name")) { if (shortLogName == null) shortLogName = computeShortName(); appendField(defaultStyle, sdbuf, field, shortLogName, fieldIdx, fieldWidths.get(field)); } else if (field.equals("name")) { appendField(defaultStyle, sdbuf, field, name, fieldIdx, fieldWidths.get(field)); } else if (field.equals("message")) { appendField(defaultStyle, sdbuf, field, MessageFormatter.basicArrayFormat(CONFIG_PARAMS.parameterStyle, messagePattern, arguments), fieldIdx, fieldWidths.get(field)); } else if (field.equals("markers")) { if(markers == null) appendField(defaultStyle, sdbuf, field, "", fieldIdx, fieldWidths.get(field)); else { appendField(defaultStyle, sdbuf, field , String.join(",", markers.stream().map(Marker::getName).toList()), fieldIdx, fieldWidths.get(field)); } } AttributedStringBuilder attrs = new AttributedStringBuilder(); StyleExpression sex = new StyleExpression(); sex.evaluate(attrs, sdbuf.toString()); buf.append(attrs.toAnsi(terminal())); } write(buf, t); } private Terminal terminal() { return TtyConfiguration.get().terminal(); } private void appendField(String defaultStyle, StringBuilder buf, String field, String value, AtomicInteger fieldIdx, int fieldWidth) { if(fieldIdx.get() > 0 && CONFIG_PARAMS.gap > 0) { buf.append(String.format("%" + CONFIG_PARAMS.gap + "s", "")); } String valueStyle = CONFIG_PARAMS.fieldStyles.get(field); int extras = countOuterCharacters(field, valueStyle); String valueText = padOrTrim(Math.max(1, fieldWidth - extras), value, field.equals("message")); if(defaultStyle != null) { valueStyle = defaultStyle.replace("${text}", valueStyle); } String ftext = valueStyle.replace("${" + field + "}", valueText); buf.append(ftext); fieldIdx.incrementAndGet(); } private int countOuterCharacters(String field, String str) { StyleExpression expr = new StyleExpression(); return WCWidth.mk_wcswidth(expr.evaluate(str.replace("${" + field + "}", "")).toString()); } private int getWidth() { int width = CONFIG_PARAMS.width; if(width == 0) { width = terminal().getWidth(); return width < 1 ? CONFIG_PARAMS.fallbackWidth : width; } else { return width; } } public void log(LoggingEvent event) { int levelInt = event.getLevel().toInt(); if (!isLevelEnabled(levelInt)) { return; } NormalizedParameters np = NormalizedParameters.normalize(event); innerHandleNormalizedLoggingCall(event.getLevel(), event.getMarkers(), np.getMessage(), np.getArguments(), event.getThrowable()); } @Override protected String getFullyQualifiedCallerName() { return null; } String padOrTrim(int width, String str, boolean valueHasStyles) { if(width == 0) return str; else { int strlen = str.length(); int paramChars = 0; if(valueHasStyles) { StyleExpression sexp = new StyleExpression(); AttributedString astr = sexp.evaluate(str); int alen = WCWidth.mk_wcswidth(astr.toString()); if(str.startsWith("Using transport")) { System.err.println("Value has styles: " + valueHasStyles + " Strlen: " +strlen + " Alen: " + alen ); } paramChars =strlen - alen; } int wwidth = WCWidth.mk_wcswidth(str) ; int diff =strlen - wwidth; if(wwidth < width) { return String.format("%-" + Math.max(1, ( width - diff )) + "s", str); } else { return str.substring(0, Math.max(0, width + paramChars - diff - CONFIG_PARAMS.ellipsisWidth)) + CONFIG_PARAMS.ellipsis; } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy