001/* 002 * jPOS Project [http://jpos.org] 003 * Copyright (C) 2000-2026 jPOS Software SRL 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU Affero General Public License as 007 * published by the Free Software Foundation, either version 3 of the 008 * License, or (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU Affero General Public License for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jpos.util; 020 021import org.jdom2.Element; 022import org.jdom2.output.Format; 023import org.jdom2.output.XMLOutputter; 024import org.jpos.jfr.LogEventDump; 025import org.jpos.log.LogRenderer; 026import org.jpos.log.LogRendererRegistry; 027import org.jpos.log.evt.SysInfo; 028import org.jpos.log.render.txt.SysInfoTxtLogRenderer; 029 030import java.io.ByteArrayOutputStream; 031import java.io.IOException; 032import java.io.PrintStream; 033import java.sql.SQLException; 034import java.time.Duration; 035import java.time.Instant; 036import java.time.LocalDateTime; 037import java.time.ZoneId; 038import java.util.*; 039 040/** 041 * A single structured log event that carries a tag, realm, payload items, and optionally a {@link Throwable}. 042 * @author @apr 043 */ 044public class LogEvent { 045 private LogSource source; 046 private String tag; 047 private final List<Object> payLoad; 048 private Instant createdAt; 049 private Instant dumpedAt; 050 private boolean honorSourceLogger; 051 private boolean noArmor; 052 private boolean hasException; 053 private Map<String,String> tags; 054 055 /** 056 * Constructs an empty event with the given tag. 057 * 058 * @param tag log tag (level/event name) 059 */ 060 public LogEvent (String tag) { 061 super(); 062 this.tag = tag; 063 createdAt = Instant.now(); 064 this.payLoad = Collections.synchronizedList (new ArrayList<>()); 065 } 066 067 /** Default constructor; uses the {@code info} tag. */ 068 public LogEvent () { 069 this("info"); 070 } 071 /** 072 * Constructs an event with the given tag and an initial payload entry. 073 * 074 * @param tag log tag (level/event name) 075 * @param msg initial payload entry 076 */ 077 public LogEvent (String tag, Object msg) { 078 this (tag); 079 addMessage(msg); 080 } 081 /** 082 * Constructs an event tied to a {@link LogSource}. 083 * 084 * @param source source whose logger and realm govern this event 085 * @param tag log tag (level/event name) 086 */ 087 public LogEvent (LogSource source, String tag) { 088 this (tag); 089 this.source = source; 090 honorSourceLogger = true; 091 } 092 /** 093 * Constructs an event tied to a {@link LogSource} with an initial payload entry. 094 * 095 * @param source source whose logger and realm govern this event 096 * @param tag log tag (level/event name) 097 * @param msg initial payload entry 098 */ 099 public LogEvent (LogSource source, String tag, Object msg) { 100 this (tag); 101 this.source = source; 102 honorSourceLogger = true; 103 addMessage(msg); 104 } 105 /** 106 * Returns the log tag. 107 * 108 * @return the event tag 109 */ 110 public String getTag() { 111 return tag; 112 } 113 /** 114 * Sets the log tag for this event. 115 * @param tag the log tag to set 116 */ 117 public void setTag (String tag) { 118 this.tag = tag; 119 } 120 /** 121 * Adds a message or object to this event's payload. 122 * @param msg the message or object to add 123 */ 124 public void addMessage (Object msg) { 125 payLoad.add (msg); 126 if (msg instanceof Throwable) 127 hasException = true; 128 } 129 /** 130 * Adds a message wrapped in an XML tag to this event's payload. 131 * @param tagname the XML tag name to wrap the message in 132 * @param message the message text 133 */ 134 public void addMessage (String tagname, String message) { 135 payLoad.add ("<"+tagname+">"+message+"</"+tagname+">"); 136 } 137 /** 138 * Returns the {@link LogSource} associated with this event, if any. 139 * 140 * @return the source, or {@code null} if not set 141 */ 142 public LogSource getSource() { 143 return source; 144 } 145 /** 146 * Replaces the {@link LogSource} associated with this event. 147 * 148 * @param source new source (may be {@code null}) 149 */ 150 public void setSource(LogSource source) { 151 this.source = source; 152 } 153 /** 154 * Controls whether the XML wrapper is suppressed in log output. 155 * @param noArmor if true, suppress the XML wrapper 156 */ 157 public void setNoArmor (boolean noArmor) { 158 this.noArmor = noArmor; 159 } 160 /** 161 * Writes the log event header to the given PrintStream. 162 * @param p the PrintStream to write the header to 163 * @param indent the indentation prefix 164 * @return the inner indentation string for nested content 165 */ 166 protected String dumpHeader (PrintStream p, String indent) { 167 if (noArmor) { 168 p.println(""); 169 } else { 170 dumpedAt = getDumpedAt(); 171 StringBuilder sb = new StringBuilder(indent); 172 sb.append ("<log realm=\""); 173 sb.append (getRealm()); 174 sb.append("\" at=\""); 175 sb.append(LocalDateTime.ofInstant(dumpedAt, ZoneId.systemDefault())); 176 sb.append ('"'); 177 long elapsed = Duration.between(createdAt, dumpedAt).toMillis(); 178 if (elapsed > 0) { 179 sb.append (" lifespan=\""); 180 sb.append (elapsed); 181 sb.append ("ms\""); 182 } 183 String traceId = tags != null ? tags.get("trace-id") : null; 184 if (traceId != null) { 185 sb.append (String.format (" trace-id=\"%s\"", traceId)); 186 } 187 sb.append ('>'); 188 p.println (sb); 189 } 190 return indent + " "; 191 } 192 /** 193 * Writes the log event trailer to the given PrintStream. 194 * @param p the PrintStream to write the trailer to 195 * @param indent the indentation prefix 196 */ 197 protected void dumpTrailer (PrintStream p, String indent) { 198 if (!noArmor) 199 p.println (indent + "</log>"); 200 } 201 /** 202 * Dumps the full log event to the given PrintStream. 203 * @param p the PrintStream to dump to 204 * @param outer the outer indentation string 205 */ 206 public void dump (PrintStream p, String outer) { 207 var jfr = new LogEventDump(); 208 jfr.begin(); 209 try { 210 String indent = dumpHeader (p, outer); 211 if (payLoad.isEmpty()) { 212 if (tag != null) 213 p.println (indent + "<" + tag + "/>"); 214 } 215 else { 216 String newIndent; 217 if (tag != null) { 218 if (!tag.isEmpty()) 219 p.println (indent + "<" + tag + ">"); 220 newIndent = indent + " "; 221 } 222 else 223 newIndent = ""; 224 synchronized (payLoad) { 225 for (Object o : payLoad) { 226 if (o instanceof Loggeable) 227 ((Loggeable) o).dump(p, newIndent); 228 else if (o instanceof SQLException) { 229 SQLException e = (SQLException) o; 230 p.println(newIndent + "<SQLException>" 231 + e.getMessage() + "</SQLException>"); 232 p.println(newIndent + "<SQLState>" 233 + e.getSQLState() + "</SQLState>"); 234 p.println(newIndent + "<VendorError>" 235 + e.getErrorCode() + "</VendorError>"); 236 ((Throwable) o).printStackTrace(p); 237 } else if (o instanceof Throwable) { 238 p.println(newIndent + "<exception name=\"" 239 + ((Throwable) o).getMessage() + "\">"); 240 p.print(newIndent); 241 ((Throwable) o).printStackTrace(p); 242 p.println(newIndent + "</exception>"); 243 } else if (o instanceof Object[]) { 244 Object[] oa = (Object[]) o; 245 p.print(newIndent + "["); 246 for (int j = 0; j < oa.length; j++) { 247 if (j > 0) 248 p.print(","); 249 p.print(oa[j].toString()); 250 } 251 p.println("]"); 252 } else if (o instanceof Element) { 253 p.println(""); 254 XMLOutputter out = new XMLOutputter(Format.getPrettyFormat()); 255 out.getFormat().setLineSeparator("\n"); 256 try { 257 out.output((Element) o, p); 258 } catch (IOException ex) { 259 ex.printStackTrace(p); 260 } 261 p.println(""); 262 } else if (o != null) { 263 LogRenderer<Object> renderer = LogRendererRegistry.getRenderer(o.getClass(), LogRenderer.Type.TXT); 264 if (renderer != null) 265 renderer.render(o, p, newIndent); 266 else 267 p.println(newIndent + o); 268 } else { 269 p.println(newIndent + "null"); 270 } 271 } 272 } 273 if (tag != null && !tag.isEmpty()) 274 p.println (indent + "</" + tag + ">"); 275 } 276 } catch (Throwable t) { 277 t.printStackTrace(p); 278 279 } finally { 280 dumpTrailer (p, outer); 281 jfr.commit(); 282 } 283 } 284 /** 285 * Returns the realm of the associated source, or empty when no source is set. 286 * 287 * @return the source's realm, or an empty string 288 */ 289 public String getRealm() { 290 return source != null ? source.getRealm() : ""; 291 } 292 /** 293 * Sets the {@code trace-id} tag explicitly. 294 * 295 * @param traceId trace identifier 296 * @return this event for chaining 297 */ 298 public LogEvent withTraceId (String traceId) { 299 return withTag("trace-id", traceId); 300 } 301 /** 302 * Sets the {@code trace-id} tag from a UUID (dashes stripped). 303 * 304 * @param uuid trace UUID 305 * @return this event for chaining 306 */ 307 public LogEvent withTraceId (UUID uuid) { 308 return withTag("trace-id", uuid.toString().replace("-", "")); 309 } 310 /** 311 * Adds or overwrites a tag on this event. 312 * 313 * @param key tag name 314 * @param value tag value 315 * @return this event for chaining 316 */ 317 public LogEvent withTag(String key, String value) { 318 if (tags == null) 319 tags = new LinkedHashMap<>(); 320 tags.put(key, value); 321 return this; 322 } 323 /** 324 * Adds the supplied tags to this event. 325 * 326 * @param map tags to add (ignored if {@code null} or empty) 327 * @return this event for chaining 328 */ 329 public LogEvent withTags(Map<String,String> map) { 330 if (map != null && !map.isEmpty()) { 331 if (tags == null) 332 tags = new LinkedHashMap<>(); 333 tags.putAll(map); 334 } 335 return this; 336 } 337 /** 338 * Returns an unmodifiable view of this event's tags. 339 * 340 * @return event tags, or an empty map if none have been set 341 */ 342 public Map<String,String> getTags() { 343 return tags != null ? Collections.unmodifiableMap(tags) : Collections.emptyMap(); 344 } 345 /** 346 * Sets the {@link LogSource} associated with this event and returns it for chaining. 347 * 348 * @param source new source 349 * @return this event for chaining 350 */ 351 public LogEvent withSource (LogSource source) { 352 setSource(source); 353 return this; 354 } 355 /** 356 * Appends a payload entry and returns this event for chaining. 357 * 358 * @param o payload entry 359 * @return this event for chaining 360 */ 361 public LogEvent add (Object o) { 362 addMessage(o); 363 return this; 364 } 365 /** 366 * Ensures a {@code trace-id} tag is present, generating one if needed. 367 * 368 * @return this event for chaining 369 */ 370 public LogEvent withTraceId () { 371 getTraceId(); 372 return this; 373 } 374 /** 375 * Returns the current trace-id, generating one if absent. 376 * 377 * @return the trace-id (never {@code null}) 378 */ 379 public String getTraceId() { 380 synchronized(getPayLoad()) { 381 String traceId = tags != null ? tags.get("trace-id") : null; 382 if (traceId == null) { 383 traceId = UUID.randomUUID().toString().replace("-",""); 384 withTag("trace-id", traceId); 385 } 386 return traceId; 387 } 388 } 389 390 /** 391 * WARNING: payLoad is a SynchronizedList. If you intend to get a reference 392 * to it in order to iterate over the list, you need to synchronize on the 393 * returned object. 394 * 395 * <pre> 396 * synchronized (evt.getPayLoad()) { 397 * Iterator iter = evt.getPayLoad().iterator(); 398 * while (iter.hasNext()) { 399 * ... 400 * ... 401 * 402 * } 403 * } 404 * </pre> 405 * @return payLoad, which is a SynchronizedList 406 */ 407 public List<Object> getPayLoad() { 408 return payLoad; 409 } 410 /** 411 * Renders this event to a string with the given indent prefix. 412 * 413 * @param indent indent prefix to apply to every emitted line 414 * @return string rendering of this event 415 */ 416 public String toString(String indent) { 417 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 418 PrintStream p = new PrintStream (baos); 419 synchronized (getPayLoad()) { 420 dump(p, indent); 421 } 422 return baos.toString(); 423 } 424 /** 425 * Renders this event to a string with no leading indent. 426 * 427 * @return string rendering of this event 428 */ 429 public String toString() { 430 return toString(""); 431 } 432 433 /** 434 * Indicates whether the payload contains a {@link Throwable}. 435 * 436 * @return {@code true} if any payload entry is a {@link Throwable} 437 */ 438 public boolean hasException() { 439 return hasException; 440 } 441 /** 442 * This is a hack for backward compatibility after accepting PR67 443 * @see <a href="https://github.com/jpos/jPOS/pull/67">PR67</a> 444 * @return true if ISOSource has been set 445 */ 446 public boolean isHonorSourceLogger() { 447 return honorSourceLogger; 448 } 449 450 /** 451 * Returns the time at which this event was first dumped, capturing it on first call. 452 * 453 * @return the dump timestamp 454 */ 455 public synchronized Instant getDumpedAt() { 456 if (dumpedAt == null) 457 dumpedAt = Instant.now(); 458 return dumpedAt; 459 } 460 /** 461 * Returns the time at which this event was constructed. 462 * 463 * @return the creation timestamp 464 */ 465 public Instant getCreatedAt() { 466 return createdAt; 467 } 468}