001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.commons.logging; 018 019import static org.apache.juneau.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023 024import java.text.*; 025import java.util.*; 026import java.util.Formatter; 027import java.util.function.*; 028import java.util.logging.*; 029 030/** 031 * LogRecord that supports lazy message formatting. 032 * 033 * <p> 034 * This class extends {@link java.util.logging.LogRecord java.util.logging.LogRecord} and overrides {@link #getMessage()} to use 035 * {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)} (printf-style <js>"%s"</js>/<js>"%d"</js>) for 036 * lazy formatting. The message is only formatted when {@link #getMessage()} is actually called. 037 * 038 * <h5 class='section'>Usage:</h5> 039 * <p> 040 * This class is used internally by {@link RichLogger} when logging formatted messages. 041 * The formatted message is only computed when the LogRecord's message is actually 042 * accessed (e.g., by a Handler or Formatter). 043 * </p> 044 * 045 * <h5 class='section'>Formatting contract:</h5> 046 * <p> 047 * Juneau {@link RichLogger} patterns are <b>printf-style</b> (rendered here via {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)}). 048 * Because this override returns the already-substituted message, downstream {@link java.util.logging.Handler Handlers} and 049 * {@link java.util.logging.Formatter Formatters} that call {@link #getMessage()} see fully-rendered text and never re-apply 050 * formatting — so the printf-vs-MessageFormat choice is purely internal. The JDK's {@link java.text.MessageFormat} 051 * parameter contract of {@link java.util.logging.LogRecord} only resurfaces if a record is serialized and consumed as a 052 * plain {@link java.util.logging.LogRecord} by a non-Juneau handler; such a consumer would apply MessageFormat to the raw 053 * <c>{0}</c> pattern, so printf patterns are not intended for that scenario. 054 * </p> 055 * 056 * <h5 class='section'>See Also:</h5><ul> 057 * <li class='jc'>{@link RichLogger} 058 * <li class='jc'>{@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)} 059 * <li class='jc'>{@link java.util.logging.LogRecord} 060 * </ul> 061 */ 062@SuppressWarnings({ 063 "java:S100", // Method names match java.util.logging.LogRecord for API compatibility 064 "java:S115", // Constants use UPPER_snakeCase convention 065 "java:S1192", // String literals intentionally duplicated for clarity 066 "java:S2143", // Date required for java.util.logging compatibility and Formatter %t conversions; not a behavior-preserving java.time swap. 067 "java:S2176" // Class name intentionally matches java.util.logging.LogRecord; extends it to enrich logging with Juneau-specific fields 068}) 069public class LogRecord extends java.util.logging.LogRecord { 070 071 private static final long serialVersionUID = 1L; 072 private final transient MessageGenerator generator; 073 074 /** 075 * Constructor. 076 * 077 * @param loggerName The logger name. Can be <jk>null</jk>. 078 * @param level The log level. Must not be <jk>null</jk>. 079 * @param msg The message pattern (will be used as-is if args is null or empty). 080 * @param parameters The format arguments, or <jk>null</jk> if none. 081 * @param throwable The throwable, or <jk>null</jk> if none. 082 */ 083 public LogRecord(String loggerName, Level level, String msg, Object[] parameters, Throwable throwable) { 084 this(loggerName, level, msg, parameters, throwable, MessageGenerator.PRINTF); 085 } 086 087 /** 088 * Constructor. 089 * 090 * @param loggerName The logger name. Can be <jk>null</jk>. 091 * @param level The log level. Must not be <jk>null</jk>. 092 * @param msg The message pattern (will be used as-is if args is null or empty). 093 * @param parameters The format arguments, or <jk>null</jk> if none. 094 * @param throwable The throwable, or <jk>null</jk> if none. 095 * @param generator The message generator. 096 */ 097 public LogRecord(String loggerName, Level level, String msg, Object[] parameters, Throwable throwable, MessageGenerator generator) { 098 super(level, msg); 099 setLoggerName(loggerName); 100 setParameters(isNotEmptyArray(parameters) ? parameters : null); 101 setThrown(throwable); 102 this.generator = generator == null ? MessageGenerator.PRINTF : generator; 103 } 104 105 /** 106 * Returns the log message, formatting it lazily if needed. 107 * 108 * <p> 109 * If parameters were set via {@link #setParameters(Object[])}, the message is formatted 110 * using {@link org.apache.juneau.commons.utils.StringUtils#format(String, Object...)} on first access. 111 * 112 * @return The log message, formatted if parameters are present. 113 */ 114 @Override 115 public String getMessage() { 116 var m = super.getMessage(); 117 var p = getParameters(); 118 if (isEmptyArray(p)) 119 return m; 120 var messageGenerator = generator == null ? MessageGenerator.PRINTF : generator; 121 return safeOptCatch(() -> messageGenerator.format(m, p), x -> m).orElse(m); 122 } 123 124 /** 125 * Formats this log record as a string using the specified format pattern. 126 * 127 * <p> 128 * Similar to how {@link SimpleFormatter} formats log records, this method 129 * allows you to specify a custom format string with placeholders that are replaced with 130 * actual values from the log record. 131 * 132 * <p> 133 * The format string supports both named placeholders (e.g., <js>"{date}"</js>) and 134 * {@link java.util.Formatter Formatter}-style format specifiers (e.g., <js>"%1$tc"</js>). 135 * Named placeholders are replaced first, then the resulting string is formatted using 136 * {@link java.util.Formatter Formatter} with the following arguments: 137 * <ol> 138 * <li><c>date</c> - The date/time as a {@link Date} object 139 * <li><c>logger</c> - The logger name 140 * <li><c>level</c> - The log level 141 * <li><c>message</c> - The formatted log message 142 * <li><c>thrown</c> - The throwable object (if any) 143 * </ol> 144 * 145 * <h5 class='section'>Named Placeholders:</h5> 146 * <ul> 147 * <li><js>"{date}"</js> - The date/time formatted using {@link SimpleFormatter SimpleFormatter}'s default date format 148 * <li><js>"{timestamp}"</js> - The date/time formatted as ISO-8601 (yyyy-MM-dd'T'HH:mm:ss.SSSZ) 149 * <li><js>"{logger}"</js> - The logger name 150 * <li><js>"{level}"</js> - The log level name 151 * <li><js>"{msg}"</js> - The log message (formatted if parameters are present) 152 * <li><js>"{thread}"</js> - The thread ID 153 * <li><js>"{threadid}"</js> - The thread ID (alias for {thread}) 154 * <li><js>"{exception}"</js> - The exception message (if thrown) 155 * <li><js>"{thrown}"</js> - The throwable object (if thrown) 156 * </ul> 157 * 158 * <h5 class='section'>Formatter-Style Format Specifiers:</h5> 159 * <p> 160 * You can use {@link java.util.Formatter Formatter}-style format specifiers directly in the format string. 161 * The arguments are passed in the same order as {@link java.util.logging.SimpleFormatter SimpleFormatter}: 162 * </p> 163 * <ul> 164 * <li><js>"%1$s"</js> or <js>"%1$tc"</js> - The date/time (argument 1) 165 * <li><js>"%2$s"</js> - The logger name (argument 2) 166 * <li><js>"%3$s"</js> - The log level (argument 3) 167 * <li><js>"%4$s"</js> - The log message (argument 4) 168 * <li><js>"%5$s"</js> - The throwable and its backtrace (argument 5) 169 * </ul> 170 * 171 * <p> 172 * For date/time formatting, you can use date/time conversion characters with argument 1: 173 * </p> 174 * <ul> 175 * <li><js>"%1$tc"</js> - Complete date and time (e.g., "Tue Mar 22 13:11:31 PDT 2011") 176 * <li><js>"%1$tb"</js> - Month abbreviation (e.g., "Mar") 177 * <li><js>"%1$td"</js> - Day of month (01-31) 178 * <li><js>"%1$tY"</js> - Year (4 digits) 179 * <li><js>"%1$tH"</js> - Hour (00-23) 180 * <li><js>"%1$tM"</js> - Minute (00-59) 181 * <li><js>"%1$tS"</js> - Second (00-59) 182 * <li><js>"%1$tL"</js> - Milliseconds (000-999) 183 * <li><js>"%1$tN"</js> - Nanoseconds (000000000-999999999, since JDK 9) 184 * <li><js>"%1$Tp"</js> - AM/PM marker (uppercase) 185 * <li><js>"%1$tp"</js> - AM/PM marker (lowercase) 186 * </ul> 187 * 188 * <p> 189 * The format string can also contain <js>"%n"</js> for newlines. 190 * 191 * <h5 class='section'>Examples:</h5> 192 * <p class='bjava'> 193 * LogRecord <jv>record</jv> = <jk>new</jk> LogRecord(Level.INFO, <js>"User {0} logged in"</js>, <js>"John"</js>); 194 * 195 * <jc>// Using named placeholders</jc> 196 * String <jv>formatted1</jv> = <jv>record</jv>.formatted(<js>"[{timestamp} {level}] {msg}"</js>); 197 * <jc>// Result: "[2025-01-22T07:42:45.123-0500 INFO] User John logged in"</jc> 198 * 199 * <jc>// Using Formatter-style specifiers</jc> 200 * String <jv>formatted2</jv> = <jv>record</jv>.formatted(<js>"%3$s: %4$s [%1$tc]%n"</js>); 201 * <jc>// Result: "INFO: User John logged in [Tue Jan 22 07:42:45 PST 2025]"</jc> 202 * 203 * <jc>// Mixed named placeholders and Formatter specifiers</jc> 204 * String <jv>formatted3</jv> = <jv>record</jv>.formatted(<js>"%1$tb %1$td, %1$tY {level}: {msg}%n"</js>); 205 * <jc>// Result: "Jan 22, 2025 INFO: User John logged in"</jc> 206 * </p> 207 * 208 * @param format The format string with placeholders and/or Formatter-style format specifiers. 209 * @return The formatted string. 210 * @see SimpleFormatter 211 * @see Formatter 212 */ 213 public String formatted(String format) { 214 return formatted(this, format); 215 } 216 217 /** 218 * Formats a JUL log record with the same placeholders as {@link #formatted(String)}. 219 * 220 * @param rec The record to format. 221 * @param format The format string. 222 * @return The formatted string. 223 */ 224 @SuppressWarnings({ 225 "deprecation" // Date constructor is deprecated but needed for compatibility 226 }) 227 public static String formatted(java.util.logging.LogRecord rec, String format) { 228 var date = new Date(rec.getMillis()); 229 230 Function<String,Object> resolver = key -> switch (key) { 231 case "date" -> "%1$s"; 232 case "logger" -> "%2$s"; 233 case "level" -> "%3$s"; 234 case "msg" -> "%4$s"; 235 case "thrown" -> "%5$s"; 236 case "timestamp" -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(date); 237 case "thread", "threadid" -> s(rec.getThreadID()); 238 case "exception" -> o(rec.getThrown()).map(x -> x.getMessage()).orElse(""); 239 default -> ""; 240 }; 241 242 return safeOptCatch(() -> f(formatNamed(format, resolver), date, rec.getLoggerName(), rec.getLevel(), rec.getMessage(), rec.getThrown()), x -> x.getLocalizedMessage()).orElse(null); 243 } 244}