Skip to main content

AddonLogger

Dedicated file logger for addon-specific logging. Writes to a separate file in Bookmap's logs directory so addon messages aren't drowned out by Bookmap's general logging. USAGE:

// In your strategy's initialize(): AddonLogger logger = new AddonLogger("PriceChannelBreakout"); // Log messages: logger.info("Strategy initialized"); logger.trade("LONG signal @ 6085.50"); logger.warn("Cooldown active"); logger.error("Failed to place order", exception); // In your strategy's stop(): logger.close(); LOG FILE LOCATION: - Windows: C:\Bookmap\Logs{addonName}{date}.log - Mac: ~/Library/Application Support/Bookmap/Logs/{addonName}{date}.log - Linux: ~/.bookmap/Logs/{addonName}_{date}.log @author Nasser

AddonLogger.java
package com.bookmap.ordermanagement.util;

import velox.api.layer1.common.DirectoryResolver;
import velox.api.layer1.common.Log;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* Dedicated file logger for addon-specific logging.
*
* Writes to a separate file in Bookmap's logs directory so addon messages
* aren't drowned out by Bookmap's general logging.
*
* USAGE:
* <pre>
* // In your strategy's initialize():
* AddonLogger logger = new AddonLogger("PriceChannelBreakout");
*
* // Log messages:
* logger.info("Strategy initialized");
* logger.trade("LONG signal @ 6085.50");
* logger.warn("Cooldown active");
* logger.error("Failed to place order", exception);
*
* // In your strategy's stop():
* logger.close();
* </pre>
*
* LOG FILE LOCATION:
* - Windows: C:\Bookmap\Logs\{addonName}_{date}.log
* - Mac: ~/Library/Application Support/Bookmap/Logs/{addonName}_{date}.log
* - Linux: ~/.bookmap/Logs/{addonName}_{date}.log
*
* @author Nasser
*/
public class AddonLogger {

/**
* Log levels for filtering verbosity.
* Higher levels include all lower levels.
*/
public enum LogLevel {
ERROR(0), // Only errors
WARN(1), // Errors + warnings
TRADE(2), // + trade events (recommended for production)
INFO(3), // + general info
DEBUG(4); // Everything (very verbose)

final int level;
LogLevel(int level) { this.level = level; }
}

private static final DateTimeFormatter FILE_DATE_FORMAT =
DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
private static final DateTimeFormatter LOG_TIMESTAMP_FORMAT =
DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

private final String addonName;
private final PrintWriter fileWriter;
private final Path logFilePath;
private final boolean mirrorToBookmapLog;
private LogLevel minLevel = LogLevel.TRADE; // Default: skip DEBUG/INFO for cleaner logs

/**
* Creates a new addon logger with default settings.
* Mirrors messages to Bookmap's log as well.
*
* @param addonName Name of the addon (used in filename and prefix)
*/
public AddonLogger(String addonName) {
this(addonName, true);
}

/**
* Creates a new addon logger.
*
* @param addonName Name of the addon (used in filename and prefix)
* @param mirrorToBookmapLog If true, also writes to Bookmap's main log
*/
public AddonLogger(String addonName, boolean mirrorToBookmapLog) {
this.addonName = addonName;
this.mirrorToBookmapLog = mirrorToBookmapLog;

// Create log file in Bookmap's logs directory
Path logsDir = DirectoryResolver.getLogsDirectory();
String timestamp = LocalDateTime.now().format(FILE_DATE_FORMAT);
String fileName = String.format("%s_%s.log", addonName, timestamp);
this.logFilePath = logsDir.resolve(fileName);

PrintWriter writer = null;
try {
// Ensure logs directory exists
Files.createDirectories(logsDir);

// Create file writer (append mode)
writer = new PrintWriter(new BufferedWriter(new FileWriter(logFilePath.toFile(), true)));

// Write header
writer.println("═══════════════════════════════════════════════════════════════");
writer.println(String.format(" %s Log - Started %s", addonName,
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))));
writer.println("═══════════════════════════════════════════════════════════════");
writer.println();
writer.flush();

Log.info(String.format("[%s] Logging to: %s", addonName, logFilePath));

} catch (IOException e) {
Log.error(String.format("[%s] Failed to create log file: %s", addonName, e.getMessage()));
}

this.fileWriter = writer;
}

// =========================================================================
// Log Level Configuration
// =========================================================================

/**
* Sets the minimum log level. Messages below this level are ignored.
* Default is TRADE (skips DEBUG and INFO for cleaner logs).
*
* @param level The minimum level to log
* @return this logger for chaining
*/
public AddonLogger setLogLevel(LogLevel level) {
this.minLevel = level;
return this;
}

/**
* Gets the current minimum log level.
*/
public LogLevel getLogLevel() {
return minLevel;
}

private boolean shouldLog(LogLevel level) {
return minLevel.level >= level.level;
}

// =========================================================================
// Logging Methods
// =========================================================================

/**
* Logs an INFO level message.
*/
public void info(String message) {
if (!shouldLog(LogLevel.INFO)) return;
log("INFO", message);
if (mirrorToBookmapLog) {
Log.info(formatPrefix() + message);
}
}

/**
* Logs an INFO level message with format arguments.
*/
public void info(String format, Object... args) {
info(String.format(format, args));
}

/**
* Logs a TRADE level message (for order/position events).
*/
public void trade(String message) {
if (!shouldLog(LogLevel.TRADE)) return;
log("TRADE", message);
if (mirrorToBookmapLog) {
Log.info(formatPrefix() + "[TRADE] " + message);
}
}

/**
* Logs a TRADE level message with format arguments.
*/
public void trade(String format, Object... args) {
trade(String.format(format, args));
}

/**
* Logs a WARN level message.
*/
public void warn(String message) {
if (!shouldLog(LogLevel.WARN)) return;
log("WARN", message);
if (mirrorToBookmapLog) {
Log.warn(formatPrefix() + message);
}
}

/**
* Logs a WARN level message with format arguments.
*/
public void warn(String format, Object... args) {
warn(String.format(format, args));
}

/**
* Logs an ERROR level message.
*/
public void error(String message) {
if (!shouldLog(LogLevel.ERROR)) return;
log("ERROR", message);
if (mirrorToBookmapLog) {
Log.error(formatPrefix() + message);
}
}

/**
* Logs an ERROR level message with exception.
*/
public void error(String message, Throwable ex) {
if (!shouldLog(LogLevel.ERROR)) return;
log("ERROR", message + " - " + ex.getMessage());
if (fileWriter != null) {
ex.printStackTrace(fileWriter);
fileWriter.flush();
}
if (mirrorToBookmapLog) {
Log.error(formatPrefix() + message, ex);
}
}

/**
* Logs an ERROR level message with format arguments.
*/
public void error(String format, Object... args) {
error(String.format(format, args));
}

/**
* Logs a DEBUG level message.
*/
public void debug(String message) {
if (!shouldLog(LogLevel.DEBUG)) return;
log("DEBUG", message);
if (mirrorToBookmapLog) {
Log.debug(formatPrefix() + message);
}
}

/**
* Logs a DEBUG level message with format arguments.
*/
public void debug(String format, Object... args) {
debug(String.format(format, args));
}

/**
* Logs a separator line for visual organization.
*/
public void separator() {
String sep = "───────────────────────────────────────────────────────────────";
log("", sep);
}

/**
* Logs a header with emphasis.
*/
public void header(String title) {
String line = "═══════════════════════════════════════════════════════════════";
log("", line);
log("", " " + title);
log("", line);
}

// =========================================================================
// Internal Methods
// =========================================================================

private void log(String level, String message) {
if (fileWriter == null) return;

String timestamp = LocalDateTime.now().format(LOG_TIMESTAMP_FORMAT);
String logLine;

if (level.isEmpty()) {
logLine = message;
} else {
logLine = String.format("%s [%-5s] %s", timestamp, level, message);
}

synchronized (fileWriter) {
fileWriter.println(logLine);
fileWriter.flush();
}
}

private String formatPrefix() {
return String.format("[%s] ", addonName);
}

/**
* Returns the path to the log file.
*/
public Path getLogFilePath() {
return logFilePath;
}

/**
* Closes the log file. Call this in your strategy's stop() method.
*/
public void close() {
if (fileWriter != null) {
fileWriter.println();
fileWriter.println("═══════════════════════════════════════════════════════════════");
fileWriter.println(String.format(" Log closed at %s",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))));
fileWriter.println("═══════════════════════════════════════════════════════════════");
fileWriter.close();

Log.info(String.format("[%s] Log file closed: %s", addonName, logFilePath));
}
}
}