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}