Orthanc/OrthancFramework/Sources/Logging.cpp
2025-06-23 19:07:37 +05:30

1071 lines
28 KiB
C++

/**
* Orthanc - A Lightweight, RESTful DICOM Store
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
* Department, University Hospital of Liege, Belgium
* Copyright (C) 2017-2023 Osimis S.A., Belgium
* Copyright (C) 2024-2025 Orthanc Team SRL, Belgium
* Copyright (C) 2021-2025 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/>.
**/
#include "PrecompiledHeaders.h"
#include "Logging.h"
#include "OrthancException.h"
#include <cassert>
#include <stdint.h>
/*********************************************************
* Common section
*********************************************************/
namespace Orthanc
{
namespace Logging
{
static const uint32_t ALL_CATEGORIES_MASK = 0xffffffff;
static uint32_t infoCategoriesMask_ = 0;
static uint32_t traceCategoriesMask_ = 0;
static std::string logTargetFolder_; // keep a track of the log folder in case of reset of the context
static std::string logTargetFile_; // keep a track of the log file in case of reset of the context
const char* EnumerationToString(LogLevel level)
{
switch (level)
{
case LogLevel_ERROR:
return "ERROR";
case LogLevel_WARNING:
return "WARNING";
case LogLevel_INFO:
return "INFO";
case LogLevel_TRACE:
return "TRACE";
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
}
LogLevel StringToLogLevel(const char *level)
{
if (strcmp(level, "ERROR") == 0)
{
return LogLevel_ERROR;
}
else if (strcmp(level, "WARNING") == 0)
{
return LogLevel_WARNING;
}
else if (strcmp(level, "INFO") == 0)
{
return LogLevel_INFO;
}
else if (strcmp(level, "TRACE") == 0)
{
return LogLevel_TRACE;
}
else
{
throw OrthancException(ErrorCode_InternalError);
}
}
void EnableInfoLevel(bool enabled)
{
if (enabled)
{
infoCategoriesMask_ = ALL_CATEGORIES_MASK;
}
else
{
// Also disable the "TRACE" level when info-level debugging is disabled
infoCategoriesMask_ = 0;
traceCategoriesMask_ = 0;
}
}
bool IsInfoLevelEnabled()
{
return (infoCategoriesMask_ != 0);
}
void EnableTraceLevel(bool enabled)
{
if (enabled)
{
// Also enable the "INFO" level when trace-level debugging is enabled
infoCategoriesMask_ = ALL_CATEGORIES_MASK;
traceCategoriesMask_ = ALL_CATEGORIES_MASK;
}
else
{
traceCategoriesMask_ = 0;
}
}
bool IsTraceLevelEnabled()
{
return (traceCategoriesMask_ != 0);
}
void SetCategoryEnabled(LogLevel level,
LogCategory category,
bool enabled)
{
// Invariant: If a bit is set for "trace", it must also be set
// for "verbose" (in other words, trace level implies verbose level)
assert((traceCategoriesMask_ & infoCategoriesMask_) == traceCategoriesMask_);
if (level == LogLevel_INFO)
{
if (enabled)
{
infoCategoriesMask_ |= static_cast<uint32_t>(category);
}
else
{
infoCategoriesMask_ &= ~static_cast<uint32_t>(category);
traceCategoriesMask_ &= ~static_cast<uint32_t>(category);
}
}
else if (level == LogLevel_TRACE)
{
if (enabled)
{
traceCategoriesMask_ |= static_cast<uint32_t>(category);
infoCategoriesMask_ |= static_cast<uint32_t>(category);
}
else
{
traceCategoriesMask_ &= ~static_cast<uint32_t>(category);
}
}
else
{
throw OrthancException(ErrorCode_ParameterOutOfRange,
"Can only modify the parameters of the INFO and TRACE levels");
}
assert((traceCategoriesMask_ & infoCategoriesMask_) == traceCategoriesMask_);
}
bool IsCategoryEnabled(LogLevel level,
LogCategory category)
{
if (level == LogLevel_ERROR ||
level == LogLevel_WARNING)
{
return true;
}
else if (level == LogLevel_INFO)
{
return (infoCategoriesMask_ & category) != 0;
}
else if (level == LogLevel_TRACE)
{
return (traceCategoriesMask_ & category) != 0;
}
else
{
return false;
}
}
bool LookupCategory(LogCategory& target,
const std::string& category)
{
if (category == "generic")
{
target = LogCategory_GENERIC;
return true;
}
else if (category == "plugins")
{
target = LogCategory_PLUGINS;
return true;
}
else if (category == "http")
{
target = LogCategory_HTTP;
return true;
}
else if (category == "dicom")
{
target = LogCategory_DICOM;
return true;
}
else if (category == "sqlite")
{
target = LogCategory_SQLITE;
return true;
}
else if (category == "jobs")
{
target = LogCategory_JOBS;
return true;
}
else if (category == "lua")
{
target = LogCategory_LUA;
return true;
}
else
{
return false;
}
}
unsigned int GetCategoriesCount()
{
return 7;
}
const char* GetCategoryName(unsigned int i)
{
if (i < GetCategoriesCount())
{
return GetCategoryName(static_cast<LogCategory>(1 << i));
}
else
{
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
}
const char* GetCategoryName(LogCategory category)
{
switch (category)
{
case LogCategory_GENERIC:
return "generic";
case LogCategory_PLUGINS:
return "plugins";
case LogCategory_HTTP:
return "http";
case LogCategory_DICOM:
return "dicom";
case LogCategory_SQLITE:
return "sqlite";
case LogCategory_JOBS:
return "jobs";
case LogCategory_LUA:
return "lua";
default:
throw OrthancException(ErrorCode_ParameterOutOfRange);
}
}
}
}
#if ORTHANC_ENABLE_LOGGING != 1
/*********************************************************
* Section if logging is disabled
*********************************************************/
namespace Orthanc
{
namespace Logging
{
void InitializePluginContext(void* pluginContext)
{
}
void InitializePluginContext(void* pluginContext, const char* pluginName)
{
}
void Initialize()
{
}
void Finalize()
{
}
void Reset()
{
}
void Flush()
{
}
void SetTargetFile(const std::string& path)
{
}
void SetTargetFolder(const std::string& path)
{
}
}
}
#elif ORTHANC_ENABLE_LOGGING_STDIO == 1
/*********************************************************
* Logger compatible with <stdio.h> OR logger that sends its
* output to the emscripten html5 api (depending on the
* definition of __EMSCRIPTEN__)
*********************************************************/
#include <stdio.h>
#ifdef __EMSCRIPTEN__
# include <emscripten/html5.h>
#endif
namespace Orthanc
{
namespace Logging
{
#ifdef __EMSCRIPTEN__
static void ErrorLogFunc(const char* msg)
{
emscripten_console_error(msg);
}
static void WarningLogFunc(const char* msg)
{
emscripten_console_warn(msg);
}
static void InfoLogFunc(const char* msg)
{
emscripten_console_log(msg);
}
static void TraceLogFunc(const char* msg)
{
emscripten_console_log(msg);
}
#else /* __EMSCRIPTEN__ not #defined */
static void ErrorLogFunc(const char* msg)
{
fprintf(stderr, "E: %s\n", msg);
}
static void WarningLogFunc(const char*)
{
fprintf(stdout, "W: %s\n", msg);
}
static void InfoLogFunc(const char*)
{
fprintf(stdout, "I: %s\n", msg);
}
static void TraceLogFunc(const char*)
{
fprintf(stdout, "T: %s\n", msg);
}
#endif /* __EMSCRIPTEN__ */
InternalLogger::~InternalLogger()
{
std::string message = messageStream_.str();
if (IsCategoryEnabled(level_, category_))
{
switch (level_)
{
case LogLevel_ERROR:
ErrorLogFunc(message.c_str());
break;
case LogLevel_WARNING:
WarningLogFunc(message.c_str());
break;
case LogLevel_INFO:
InfoLogFunc(message.c_str());
// TODO: stone_console_info(message_.c_str());
break;
case LogLevel_TRACE:
TraceLogFunc(message.c_str());
break;
default:
{
std::stringstream ss;
ss << "Unknown log level (" << level_ << ") for message: " << message;
std::string s = ss.str();
ErrorLogFunc(s.c_str());
}
}
}
}
void InitializePluginContext(void* pluginContext)
{
}
void InitializePluginContext(void* pluginContext, const char* pluginName)
{
}
void Initialize()
{
}
void Finalize()
{
}
void Reset()
{
}
void Flush()
{
}
void SetTargetFile(const std::string& path)
{
}
void SetTargetFolder(const std::string& path)
{
}
}
}
#else
/*********************************************************
* Logger compatible with the Orthanc plugin SDK, or that
* mimics behavior from Google Log.
*********************************************************/
#include <boost/thread/thread.hpp>
#include <cassert>
namespace
{
/**
* This is minimal implementation of the context for an Orthanc
* plugin, limited to the logging facilities, and that is binary
* compatible with the definitions of "OrthancCPlugin.h"
**/
typedef enum
{
_OrthancPluginService_LogInfo = 1,
_OrthancPluginService_LogWarning = 2,
_OrthancPluginService_LogError = 3,
_OrthancPluginService_LogMessage = 45,
_OrthancPluginService_INTERNAL = 0x7fffffff
} _OrthancPluginService;
typedef struct _OrthancPluginContext_t
{
void* pluginsManager;
const char* orthancVersion;
void (*Free) (void* buffer);
int32_t (*InvokeService) (struct _OrthancPluginContext_t* context,
_OrthancPluginService service,
const void* params);
} OrthancPluginContext;
typedef struct
{
const char* message;
const char* plugin;
const char* file;
uint32_t line;
uint32_t category; // can be a LogCategory or a OrthancPluginLogCategory
uint32_t level; // can be a LogLevel or a OrthancPluginLogLevel
} _OrthancPluginLogMessage;
}
#include "Enumerations.h"
#include "SystemToolbox.h"
#include "Toolbox.h"
#include <fstream>
#include <boost/filesystem.hpp>
#include <boost/thread.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>
namespace
{
struct LoggingStreamsContext
{
std::string targetFile_;
std::string targetFolder_;
std::ostream* error_;
std::ostream* warning_;
std::ostream* info_;
std::unique_ptr<std::ofstream> file_;
LoggingStreamsContext() :
error_(&std::cerr),
warning_(&std::cerr),
info_(&std::cerr)
{
}
};
}
static std::unique_ptr<LoggingStreamsContext> loggingStreamsContext_;
static boost::mutex loggingStreamsMutex_;
static Orthanc::Logging::NullStream nullStream_;
static OrthancPluginContext* pluginContext_ = NULL; // this is != NULL only when running from a plugin
static std::string pluginName_; // this string can only be non-empty if running from a plugin
static bool hasOrthancAdvancedLogging_ = false; // Whether the Orthanc runtime is >= 1.12.4
static boost::recursive_mutex threadNamesMutex_;
static std::map<boost::thread::id, std::string> threadNames_;
static bool enableThreadNames_ = true;
namespace Orthanc
{
namespace Logging
{
void EnableThreadNames(bool enabled)
{
enableThreadNames_ = enabled;
}
static void GetLogPath(boost::filesystem::path& log,
boost::filesystem::path& link,
const std::string& suffix,
const std::string& directory)
{
/**
From Google Log documentation:
Unless otherwise specified, logs will be written to the filename
"<program name>.<hostname>.<user name>.log<suffix>.",
followed by the date, time, and pid (you can't prevent the date,
time, and pid from being in the filename).
In this implementation : "hostname" and "username" are not used
**/
boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
boost::filesystem::path root(directory);
boost::filesystem::path exe(SystemToolbox::GetPathToExecutable());
if (!boost::filesystem::exists(root) ||
!boost::filesystem::is_directory(root))
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
char date[64];
sprintf(date, "%04d%02d%02d-%02d%02d%02d.%d",
static_cast<int>(now.date().year()),
now.date().month().as_number(),
now.date().day().as_number(),
static_cast<int>(now.time_of_day().hours()),
static_cast<int>(now.time_of_day().minutes()),
static_cast<int>(now.time_of_day().seconds()),
SystemToolbox::GetProcessId());
std::string programName = exe.filename().replace_extension("").string();
log = (root / (programName + ".log" + suffix + "." + std::string(date)));
link = (root / (programName + ".log" + suffix));
}
static void PrepareLogFolder(std::unique_ptr<std::ofstream>& file,
const std::string& suffix,
const std::string& directory)
{
boost::filesystem::path log, link;
GetLogPath(log, link, suffix, directory);
#if !defined(_WIN32) && (defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__)))
boost::filesystem::remove(link);
boost::filesystem::create_symlink(log.filename(), link);
#endif
file.reset(new std::ofstream(log.string().c_str()));
}
// "loggingStreamsMutex_" must be locked
static void CheckFile(std::unique_ptr<std::ofstream>& f)
{
if (loggingStreamsContext_->file_.get() == NULL ||
!loggingStreamsContext_->file_->is_open())
{
throw OrthancException(ErrorCode_CannotWriteFile);
}
}
void SetCurrentThreadNameInternal(const boost::thread::id& id, const std::string& name)
{
boost::recursive_mutex::scoped_lock lock(threadNamesMutex_);
if (name.size() > 16)
{
throw OrthancException(ErrorCode_InternalError, std::string("Thread name can not exceed 16 characters: ") + name);
}
threadNames_[id] = name;
}
void SetCurrentThreadName(const std::string& name)
{
boost::recursive_mutex::scoped_lock lock(threadNamesMutex_);
SetCurrentThreadNameInternal(boost::this_thread::get_id(), name);
}
bool HasCurrentThreadName()
{
boost::thread::id threadId = boost::this_thread::get_id();
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
return threadNames_.find(threadId) != threadNames_.end();
}
static std::string GetCurrentThreadName()
{
boost::thread::id threadId = boost::this_thread::get_id();
boost::recursive_mutex::scoped_lock lock(threadNamesMutex_);
if (threadNames_.find(threadId) == threadNames_.end())
{
// set the threadId as the thread name
SetCurrentThreadNameInternal(threadId, boost::lexical_cast<std::string>(threadId));
}
return threadNames_[threadId];
}
static void GetLinePrefix(std::string& prefix,
LogLevel level,
const char* pluginName, // when logging in the core but coming from a plugin, pluginName_ is NULL but this argument is != NULL
const char* file,
int line,
LogCategory category)
{
boost::filesystem::path path(file);
boost::posix_time::ptime now = boost::posix_time::microsec_clock::local_time();
boost::posix_time::time_duration duration = now.time_of_day();
/**
From Google Log documentation:
"Log lines have this form:
Lmmdd hh:mm:ss.uuuuuu threadid file:line] msg...
where the fields are defined as follows:
L A single character, representing the log level (eg 'I' for INFO)
mm The month (zero padded; ie May is '05')
dd The day (zero padded)
hh:mm:ss.uuuuuu Time in hours, minutes and fractional seconds
threadid The space-padded thread ID as returned by GetTID() (this matches the PID on Linux)
file The file name
line The line number
msg The user-supplied message"
In this implementation, "threadid" is not printed.
**/
char c;
switch (level)
{
case LogLevel_ERROR:
c = 'E';
break;
case LogLevel_WARNING:
c = 'W';
break;
case LogLevel_INFO:
c = 'I';
break;
case LogLevel_TRACE:
c = 'T';
break;
default:
c = '?';
break;
}
char date[64];
sprintf(date, "%c%02d%02d %02d:%02d:%02d.%06d ",
c,
now.date().month().as_number(),
now.date().day().as_number(),
static_cast<int>(duration.hours()),
static_cast<int>(duration.minutes()),
static_cast<int>(duration.seconds()),
static_cast<int>(duration.fractional_seconds()));
char threadName[20]; // thread names are limited to 16 char + a space
if (enableThreadNames_)
{
sprintf(threadName, "%16s ", GetCurrentThreadName().c_str());
}
else
{
threadName[0] = '\0';
}
std::string internalPluginName = "";
if (pluginName != NULL)
{
internalPluginName = std::string(pluginName) + ":/";
}
prefix = (std::string(date) + threadName + internalPluginName + path.filename().string() + ":" +
boost::lexical_cast<std::string>(line) + "] ");
if (level != LogLevel_ERROR &&
level != LogLevel_WARNING &&
category != LogCategory_GENERIC)
{
prefix += "(" + std::string(GetCategoryName(category)) + ") ";
}
}
void InitializePluginContext(void* pluginContext)
{
assert(sizeof(_OrthancPluginService) == sizeof(int32_t));
if (pluginContext == NULL)
{
throw OrthancException(ErrorCode_NullPointer);
}
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
loggingStreamsContext_.reset(NULL);
pluginContext_ = reinterpret_cast<OrthancPluginContext*>(pluginContext);
// The value "hasOrthancAdvancedLogging_" is cached to avoid computing it on every logged message
hasOrthancAdvancedLogging_ = Toolbox::IsVersionAbove(pluginContext_->orthancVersion, 1, 12, 4);
EnableInfoLevel(true); // allow the plugin to log at info level (but the Orthanc Core still decides of the level)
}
void InitializePluginContext(void* pluginContext, const std::string& pluginName)
{
InitializePluginContext(pluginContext);
pluginName_ = pluginName;
}
void Initialize()
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
if (loggingStreamsContext_.get() == NULL)
{
loggingStreamsContext_.reset(new LoggingStreamsContext);
}
}
void Finalize()
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
loggingStreamsContext_.reset(NULL);
}
void Reset()
{
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
loggingStreamsContext_.reset(new LoggingStreamsContext);
}
// Recover the old logging context if any
if (!logTargetFile_.empty())
{
SetTargetFile(logTargetFile_);
}
else if (!logTargetFolder_.empty())
{
SetTargetFolder(logTargetFolder_);
}
}
void SetTargetFolder(const std::string& path)
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
if (loggingStreamsContext_.get() != NULL)
{
PrepareLogFolder(loggingStreamsContext_->file_, "" /* no suffix */, path);
CheckFile(loggingStreamsContext_->file_);
loggingStreamsContext_->targetFile_.clear();
loggingStreamsContext_->targetFolder_ = path;
loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get();
loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get();
loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get();
logTargetFolder_ = path;
}
}
void SetTargetFile(const std::string& path)
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
if (loggingStreamsContext_.get() != NULL)
{
loggingStreamsContext_->file_.reset(new std::ofstream(path.c_str(), std::fstream::app));
CheckFile(loggingStreamsContext_->file_);
loggingStreamsContext_->targetFile_ = path;
loggingStreamsContext_->targetFolder_.clear();
loggingStreamsContext_->warning_ = loggingStreamsContext_->file_.get();
loggingStreamsContext_->error_ = loggingStreamsContext_->file_.get();
loggingStreamsContext_->info_ = loggingStreamsContext_->file_.get();
logTargetFile_ = path;
}
}
InternalLogger::InternalLogger(LogLevel level,
LogCategory category,
const char* pluginName,
const char* file,
int line) :
lock_(loggingStreamsMutex_, boost::defer_lock_t()),
level_(level),
stream_(&nullStream_), // By default, logging to "/dev/null" is simulated
category_(category),
file_(file),
line_(line)
{
if (pluginContext_ != NULL)
{
// We are logging using the Orthanc plugin SDK
if (level_ == LogLevel_TRACE ||
!IsCategoryEnabled(level_, category))
{
// No trace level in plugins, directly exit as the stream is
// set to "/dev/null"
return;
}
else
{
pluginStream_.reset(new std::stringstream);
stream_ = pluginStream_.get();
}
}
else
{
// We are logging in a standalone application, not inside an Orthanc plugin
if (!IsCategoryEnabled(level_, category))
{
// This logging level is disabled, directly exit as the
// stream is set to "/dev/null"
return;
}
std::string prefix;
GetLinePrefix(prefix, level_, pluginName, file, line, category);
{
// We lock the global mutex. The mutex is locked until the
// destructor is called: No change in the output can be done.
lock_.lock();
if (loggingStreamsContext_.get() == NULL)
{
// Have you called Orthanc::Logging::InitializePluginContext()?
fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine "
"(or did you forgot to initialize it?)\n");
lock_.unlock();
return;
}
switch (level_)
{
case LogLevel_ERROR:
stream_ = loggingStreamsContext_->error_;
break;
case LogLevel_WARNING:
stream_ = loggingStreamsContext_->warning_;
break;
case LogLevel_INFO:
case LogLevel_TRACE:
stream_ = loggingStreamsContext_->info_;
break;
default: // Should not occur
stream_ = loggingStreamsContext_->error_;
break;
}
if (stream_ == &nullStream_)
{
// The logging is disabled for this level, we can release
// the global mutex.
lock_.unlock();
}
else
{
try
{
(*stream_) << prefix;
}
catch (...)
{
// Something is going really wrong, probably running out of
// memory. Fallback to a degraded mode.
stream_ = loggingStreamsContext_->error_;
(*stream_) << "E???? ??:??:??.?????? ] ";
}
}
}
}
}
InternalLogger::~InternalLogger()
{
if (pluginStream_.get() != NULL)
{
// We are logging through the Orthanc SDK
std::string message = pluginStream_->str();
if (pluginContext_ != NULL)
{
if (!pluginName_.empty() &&
hasOrthancAdvancedLogging_)
{
_OrthancPluginLogMessage m;
m.category = category_;
m.level = level_;
m.file = file_;
m.line = line_;
m.plugin = pluginName_.c_str();
m.message = message.c_str();
pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogMessage, &m);
}
else
{
switch (level_)
{
case LogLevel_ERROR:
pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str());
break;
case LogLevel_WARNING:
pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str());
break;
case LogLevel_INFO:
pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
break;
default:
break;
}
}
}
}
else if (stream_ != &nullStream_)
{
*stream_ << "\n";
stream_->flush();
}
}
void Flush()
{
if (pluginContext_ != NULL)
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
if (loggingStreamsContext_.get() != NULL &&
loggingStreamsContext_->file_.get() != NULL)
{
loggingStreamsContext_->file_->flush();
}
}
}
void SetErrorWarnInfoLoggingStreams(std::ostream& errorStream,
std::ostream& warningStream,
std::ostream& infoStream)
{
boost::mutex::scoped_lock lock(loggingStreamsMutex_);
loggingStreamsContext_.reset(new LoggingStreamsContext);
loggingStreamsContext_->error_ = &errorStream;
loggingStreamsContext_->warning_ = &warningStream;
loggingStreamsContext_->info_ = &infoStream;
}
}
}
#endif // ORTHANC_ENABLE_LOGGING