001/* 002 * Copyright 2025 devteam@scivicslab.com 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, 011 * software distributed under the License is distributed on an 012 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 013 * either express or implied. See the License for the 014 * specific language governing permissions and limitations 015 * under the License. 016 */ 017 018package com.scivicslab.actoriac.accumulator; 019 020import java.time.LocalDateTime; 021import java.time.ZoneId; 022import java.time.format.DateTimeFormatter; 023import java.util.logging.Handler; 024import java.util.logging.LogRecord; 025 026import org.json.JSONObject; 027 028import com.scivicslab.pojoactor.workflow.IIActorRef; 029import com.scivicslab.pojoactor.workflow.IIActorSystem; 030 031/** 032 * A java.util.logging Handler that forwards log messages to MultiplexerAccumulatorIIAR. 033 * 034 * <p>This handler bridges java.util.logging with the MultiplexerAccumulator system, 035 * allowing all log messages to be captured in the same output destinations 036 * (console, file, database) as command output and cowsay.</p> 037 * 038 * <h2>Log Format</h2> 039 * <p>Log messages are formatted as:</p> 040 * <pre>{@code 041 * 2026-01-17T12:27:54+09:00 INFO Starting workflow: main-collect-sysinfo 042 * }</pre> 043 * 044 * <h2>Message Format</h2> 045 * <p>Messages sent to MultiplexerAccumulator:</p> 046 * <pre>{@code 047 * { 048 * "source": "cli", 049 * "type": "log-INFO", 050 * "data": "2026-01-17T12:27:54+09:00 INFO Starting workflow: main-collect-sysinfo" 051 * } 052 * }</pre> 053 * 054 * <h2>Usage</h2> 055 * <pre>{@code 056 * // After registering MultiplexerAccumulatorIIAR 057 * MultiplexerLogHandler logHandler = new MultiplexerLogHandler(system); 058 * Logger.getLogger("").addHandler(logHandler); 059 * }</pre> 060 * 061 * @author devteam@scivicslab.com 062 * @since 2.12.0 063 */ 064public class MultiplexerLogHandler extends Handler { 065 066 private static final DateTimeFormatter ISO_FORMATTER = 067 DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); 068 private static final ZoneId SYSTEM_ZONE = ZoneId.systemDefault(); 069 070 private final IIActorSystem system; 071 private volatile boolean closed = false; 072 073 /** 074 * Constructs a MultiplexerLogHandler. 075 * 076 * @param system the actor system to retrieve outputMultiplexer from 077 */ 078 public MultiplexerLogHandler(IIActorSystem system) { 079 this.system = system; 080 } 081 082 @Override 083 public void publish(LogRecord record) { 084 if (closed || record == null) { 085 return; 086 } 087 088 // Skip if log level is not loggable 089 if (!isLoggable(record)) { 090 return; 091 } 092 093 // Get the multiplexer actor (lazy lookup for loose coupling) 094 IIActorRef<?> multiplexer = system.getIIActor("outputMultiplexer"); 095 if (multiplexer == null) { 096 // Multiplexer not yet registered, skip 097 return; 098 } 099 100 // Format the log message 101 String timestamp = LocalDateTime.now().atZone(SYSTEM_ZONE).format(ISO_FORMATTER); 102 String level = record.getLevel().getName(); 103 String message = formatMessage(record); 104 String formattedLog = String.format("%s %s %s", timestamp, level, message); 105 106 // Send to multiplexer 107 try { 108 JSONObject arg = new JSONObject(); 109 arg.put("source", getSourceName(record)); 110 arg.put("type", "log-" + level); 111 arg.put("data", formattedLog); 112 multiplexer.callByActionName("add", arg.toString()); 113 } catch (Exception e) { 114 // Avoid infinite recursion - don't log errors from the log handler 115 System.err.println("MultiplexerLogHandler error: " + e.getMessage()); 116 } 117 } 118 119 /** 120 * Formats the log message, including any throwable if present. 121 */ 122 private String formatMessage(LogRecord record) { 123 String message = record.getMessage(); 124 125 // Handle parameterized messages 126 Object[] params = record.getParameters(); 127 if (params != null && params.length > 0) { 128 try { 129 message = String.format(message, params); 130 } catch (Exception e) { 131 // If formatting fails, use the raw message 132 } 133 } 134 135 // Append throwable if present 136 Throwable thrown = record.getThrown(); 137 if (thrown != null) { 138 StringBuilder sb = new StringBuilder(message); 139 sb.append("\n").append(thrown.getClass().getName()); 140 if (thrown.getMessage() != null) { 141 sb.append(": ").append(thrown.getMessage()); 142 } 143 for (StackTraceElement element : thrown.getStackTrace()) { 144 sb.append("\n\tat ").append(element); 145 } 146 message = sb.toString(); 147 } 148 149 return message; 150 } 151 152 /** 153 * Determines the source name from the log record. 154 */ 155 private String getSourceName(LogRecord record) { 156 String loggerName = record.getLoggerName(); 157 if (loggerName == null || loggerName.isEmpty()) { 158 return "system"; 159 } 160 161 // Use short name for common patterns 162 if (loggerName.startsWith("com.scivicslab.actoriac.cli")) { 163 return "cli"; 164 } 165 if (loggerName.startsWith("com.scivicslab.actoriac")) { 166 return "actor-iac"; 167 } 168 if (loggerName.startsWith("com.scivicslab.pojoactor")) { 169 return "pojo-actor"; 170 } 171 172 // Return the last part of the logger name 173 int lastDot = loggerName.lastIndexOf('.'); 174 if (lastDot >= 0 && lastDot < loggerName.length() - 1) { 175 return loggerName.substring(lastDot + 1); 176 } 177 178 return loggerName; 179 } 180 181 @Override 182 public void flush() { 183 // No buffering, nothing to flush 184 } 185 186 @Override 187 public void close() throws SecurityException { 188 closed = true; 189 } 190}