package de.opcua.app.logging; import java.io.*; import java.nio.file.*; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.locks.ReentrantLock; import java.util.zip.GZIPOutputStream; /** * JavaScript execution logger with rotation and debug levels */ public class ScriptLogger { public enum Level { DEBUG(0), INFO(1), WARN(2), ERROR(3), OFF(4); private final int value; Level(int value) { this.value = value; } public int getValue() { return value; } } private final String logDirectory; private final String logFileName; private final long maxFileSize; private final int maxBackupFiles; private Level currentLevel; private boolean enabled; private PrintWriter writer; private long currentFileSize; private final ReentrantLock lock; private final DateTimeFormatter formatter; public ScriptLogger(String logDirectory, boolean enabled, Level level) { this.logDirectory = logDirectory; this.logFileName = "javascript.log"; this.maxFileSize = 10 * 1024 * 1024; // 10 MB this.maxBackupFiles = 5; this.currentLevel = level; this.enabled = enabled; this.lock = new ReentrantLock(); this.formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); if (enabled) { initializeLogger(); } } private void initializeLogger() { try { Path logDir = Paths.get(logDirectory); if (!Files.exists(logDir)) { Files.createDirectories(logDir); } Path logFile = Paths.get(logDirectory, logFileName); if (Files.exists(logFile)) { currentFileSize = Files.size(logFile); } else { currentFileSize = 0; } writer = new PrintWriter(new BufferedWriter( new FileWriter(logFile.toFile(), true)), true); log(Level.INFO, "JavaScript logger initialized"); } catch (IOException e) { System.err.println("[ScriptLogger] Failed to initialize: " + e.getMessage()); enabled = false; } } public void setEnabled(boolean enabled) { lock.lock(); try { if (enabled && !this.enabled) { initializeLogger(); } else if (!enabled && this.enabled) { closeLogger(); } this.enabled = enabled; } finally { lock.unlock(); } } public void setLevel(Level level) { this.currentLevel = level; } public boolean isEnabled() { return enabled; } public Level getLevel() { return currentLevel; } public void debug(String message, Object... args) { log(Level.DEBUG, message, args); } public void info(String message, Object... args) { log(Level.INFO, message, args); } public void warn(String message, Object... args) { log(Level.WARN, message, args); } public void error(String message, Object... args) { log(Level.ERROR, message, args); } public void error(String message, Throwable throwable) { if (!shouldLog(Level.ERROR)) return; log(Level.ERROR, message); lock.lock(); try { if (writer != null) { throwable.printStackTrace(writer); writer.flush(); } } finally { lock.unlock(); } } private void log(Level level, String message, Object... args) { if (!shouldLog(level)) return; String formattedMessage = args.length > 0 ? String.format(message, args) : message; String logLine = String.format("[%s] [%s] %s%n", LocalDateTime.now().format(formatter), level.name(), formattedMessage ); lock.lock(); try { if (writer != null) { writer.print(logLine); writer.flush(); currentFileSize += logLine.length(); if (currentFileSize >= maxFileSize) { rotateLog(); } } // Also print to console in debug mode if (level == Level.DEBUG || level == Level.ERROR) { System.out.print(logLine); } } finally { lock.unlock(); } } private boolean shouldLog(Level level) { return enabled && level.getValue() >= currentLevel.getValue(); } private void rotateLog() { try { closeLogger(); Path currentLog = Paths.get(logDirectory, logFileName); // Delete oldest backup if exists Path oldestBackup = Paths.get(logDirectory, logFileName + "." + maxBackupFiles + ".gz"); if (Files.exists(oldestBackup)) { Files.delete(oldestBackup); } // Shift existing backups for (int i = maxBackupFiles - 1; i >= 1; i--) { Path from = Paths.get(logDirectory, logFileName + "." + i + ".gz"); Path to = Paths.get(logDirectory, logFileName + "." + (i + 1) + ".gz"); if (Files.exists(from)) { Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); } } // Compress and move current log to .1.gz Path backupLog = Paths.get(logDirectory, logFileName + ".1.gz"); compressFile(currentLog, backupLog); // Delete original Files.delete(currentLog); // Reinitialize currentFileSize = 0; writer = new PrintWriter(new BufferedWriter( new FileWriter(currentLog.toFile(), true)), true); log(Level.INFO, "Log rotated - new file created"); } catch (IOException e) { System.err.println("[ScriptLogger] Failed to rotate log: " + e.getMessage()); } } private void compressFile(Path source, Path target) throws IOException { try (FileInputStream fis = new FileInputStream(source.toFile()); FileOutputStream fos = new FileOutputStream(target.toFile()); GZIPOutputStream gzipOS = new GZIPOutputStream(fos)) { byte[] buffer = new byte[8192]; int len; while ((len = fis.read(buffer)) != -1) { gzipOS.write(buffer, 0, len); } } } private void closeLogger() { if (writer != null) { writer.close(); writer = null; } } public void close() { lock.lock(); try { closeLogger(); } finally { lock.unlock(); } } /** * Log script execution with context */ public void logScriptExecution(String actionName, String nodeId, String trigger, String value, boolean success, String result) { if (!enabled) return; Level level = success ? Level.INFO : Level.ERROR; log(level, "Script: %s | Node: %s | Trigger: %s | Value: %s | Success: %s | Result: %s", actionName, nodeId, trigger, value, success, result ); } /** * Get current log file content (last N lines) */ public String getRecentLogs(int lines) { Path logFile = Paths.get(logDirectory, logFileName); if (!Files.exists(logFile)) { return "No logs available"; } try { java.util.List allLines = Files.readAllLines(logFile); int start = Math.max(0, allLines.size() - lines); return String.join("\n", allLines.subList(start, allLines.size())); } catch (IOException e) { return "Error reading logs: " + e.getMessage(); } } }