Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
Log.cpp 7.98 KiB
/********************************************************************************
 * Copyright (c) 2023 CEA-List
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 ********************************************************************************/

#include "aidge/utils/Log.hpp"

#include <fmt/core.h>

#include <cstdlib>  // std::getenv
#include <memory>
#include <string>
#include <vector>

namespace Aidge {

/**
 * @brief Initialize console log level from environment. If compile mode is
 * DEBUG, then the default level is Log::Level::Debug, else it is
 * Log::Level::Notice.
 *
 * WARNING: Do not use this variable directly, use getConsoleLevel() instead.
 */
Log::Level Log::mConsoleLevel = []() {
#ifndef NDEBUG
    constexpr Level defaultLevel = Level::Debug;
#else
    constexpr Log::Level defaultLevel = Level::Notice;
#endif
    if (const char* level = std::getenv("AIDGE_LOGLEVEL_CONSOLE")) {
        return level[0] == 'D' ? Debug :
               level[0] == 'I' ? Info :
               level[0] == 'N' ? Notice :
               level[0] == 'W' ? Warn :
               level[0] == 'E' ? Error :
               level[0] == 'F' ? Fatal : defaultLevel;
    }
    return defaultLevel;
}();

/**
 * @brief Initialize color setting from environment or default to enabled
 */
bool Log::mConsoleColor = []() {
    const char* color = std::getenv("AIDGE_LOG_COLOR");
    return !color || (std::string(color) != "off" &&
                     std::string(color) != "OFF" &&
                     std::string(color) != "0");
}();

/**
 * @brief Initialize file log level from environment. If compile mode is DEBUG,
 * then the default level is Log::Level::Debug, else it is Log::Level::Notice.
 */
Log::Level Log::mFileLevel = []() {
#ifndef NDEBUG
    constexpr Log::Level defaultLevel = Level::Debug;
#else
    constexpr Log::Level defaultLevel = Level::Notice;
#endif
    if (const char* level = std::getenv("AIDGE_LOGLEVEL_FILE")) {
        return level[0] == 'D' ? Debug :
               level[0] == 'I' ? Info :
               level[0] == 'N' ? Notice :
               level[0] == 'W' ? Warn :
               level[0] == 'E' ? Error :
               level[0] == 'F' ? Fatal : defaultLevel;
    }
    return defaultLevel;
}();

/**
 * @brief Initialize file path from environment
 */
std::string Log::mFileName = []() {
    const char* file = std::getenv("AIDGE_LOG_FILE");
    return file ? std::string(file) : std::string();
}();

std::unique_ptr<FILE, Log::fcloseDeleter> Log::mFile{nullptr};
std::vector<std::string> Log::mContext;
int Log::mFloatingPointPrecision = 5;

/**
 * @brief Internal logging implementation
 * @param level Severity level of the message
 * @param msg The message to log
 */
void Log::log(Level level, const std::string& msg) {
    /**
     * @brief Helper function to wrap text to a specified width, respecting
     * explicit line breaks (\n) and accounting for ANSI escape sequences.
     */
    const auto wrapText = [](const std::string& text, const std::size_t width) -> std::vector<std::string> {
        std::vector<std::string> wrappedLines;
        std::size_t start = 0;

        while (start < text.size()) {
            std::size_t lineWidth = 0;
            std::size_t current = start;

            while (current < text.size() && lineWidth < width) {
                if (text[current] == '\033') {
                    // Found ANSI escape sequence, skip until 'm'
                    std::size_t ansiEnd = text.find('m', current);
                    if (ansiEnd != std::string::npos) {
                        // Add the length of the ANSI sequence to the width allowance
                        // width += (ansiEnd - current + 1);
                        current = ansiEnd + 1;
                    } else {
                        // Malformed sequence, treat as normal characters
                        ++current;
                    }
                } else if (text[current] == '\n') {
                    // Handle explicit line break
                    break;
                } else {
                    // Normal character, increase line width
                    ++lineWidth;
                    ++current;
                }
            }

            // Find the end of the current line
            std::size_t lineEnd = current;

            // Adjust for spaces if needed
            if (lineEnd < text.size() && text[lineEnd] != '\n') {
                std::size_t lastSpace = text.rfind(' ', lineEnd);
                if (lastSpace != std::string::npos && lastSpace > start) {
                    lineEnd = lastSpace;
                }
            }
            // Add the wrapped line to the result
            wrappedLines.push_back(text.substr(start, lineEnd - start));

            start = ++lineEnd;
        }

        return wrappedLines;
    };


    // Get the terminal color for the log level
    const auto getColor = [](Level lvl) {
        switch (lvl) {
            case Debug:   return fmt::terminal_color::green;
            case Info:    return fmt::terminal_color::blue;
            case Notice:  return fmt::terminal_color::bright_blue;
            case Warn:    return fmt::terminal_color::yellow;
            case Error:   return fmt::terminal_color::red;
            case Fatal:   return fmt::terminal_color::bright_magenta;
            default:      return fmt::terminal_color::white;
        }
    };

    // Get the string representation of the log level
    const auto levelStr = EnumStrings<Level>::data[static_cast<std::size_t>(level)];
    const std::size_t levelIndentSizes[6] = {10, 9, 11, 12, 10, 10};
    const std::size_t width = 80 - levelIndentSizes[static_cast<std::size_t>(level)];

    if (level >= getConsoleLevel()) {
        for (const auto& context : mContext) {
            fmt::println("Context: {}", context);
        }

        // Wrap the message and print each line
        auto wrappedLines = wrapText(msg, width);
        for (std::size_t i = 0; i < wrappedLines.size(); ++i) {
            if (mConsoleColor) {
                if (i == 0) {
                    fmt::print("[");
                    fmt::print(fg(getColor(level)), "{}", levelStr);
                    fmt::print("] - {}\n", wrappedLines[i]);
                } else {
                    fmt::print("[");
                    fmt::print(fg(getColor(level)), "{}", levelStr);
                    fmt::print("]   {}\n", wrappedLines[i]);
                }
            } else {
                if (i == 0) {
                    fmt::print("[{}] - {}\n", levelStr, wrappedLines[i]);
                } else {
                    fmt::print("[{}]   {}\n", levelStr, wrappedLines[i]);
                }
            }
        }
    }

    if (level >= mFileLevel && !mFileName.empty()) {
        if (!mFile) {
            initFile(mFileName);
        }

        for (const auto& context : mContext) {
            fmt::println(mFile.get(), "Context: {}", context);
        }

        auto wrappedLines = wrapText(msg, width);
        for (std::size_t i = 0; i < wrappedLines.size(); ++i) {
            if (i == 0) {
                fmt::println(mFile.get(), "{}: {}", levelStr, wrappedLines[i]);
            } else {
                fmt::println(mFile.get(), "{}   {}", levelStr, wrappedLines[i]);
            }
        }
    }
}



/**
 * @brief Initialize or re-initialize the log file
 * @param fileName Path to the log file
 * @throw std::runtime_error if file cannot be opened
 */
void Log::initFile(const std::string& fileName) {
    if (FILE* file = std::fopen(fileName.c_str(), "a")) {
        mFile.reset(file);
    } else {
        mFileName.clear();
        throw std::runtime_error(
            fmt::format("Could not create log file: {}", fileName));
    }
}

void Log::setFileName(const std::string& fileName) {
    if (fileName != mFileName) {
        mFileName = fileName;
        mFile.reset();
        if (!fileName.empty()) {
            initFile(fileName);
        }
    }
}

} // namespace Aidge