changeset 6542:6b9133fa6e88

logged errors/warnings metrics
author Alain Mazy <am@orthanc.team>
date Wed, 10 Dec 2025 12:01:24 +0100
parents 770d6037b79f
children d12e34b27f11
files NEWS OrthancFramework/Sources/Logging.cpp OrthancFramework/Sources/Logging.h OrthancServer/Sources/main.cpp TODO
diffstat 5 files changed, 87 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Dec 09 14:51:25 2025 +0100
+++ b/NEWS	Wed Dec 10 12:01:24 2025 +0100
@@ -4,6 +4,13 @@
 * Upgraded dependencies for static builds:
   - boost 1.89.0
 
+REST API
+--------
+
+* Added new metrics in "/tools/metrics-prometheus":
+  - "orthanc_logged_errors_count" 
+  - "orthanc_logged_warnings_count"
+
 
 Version 1.12.10 (2025-11-26)
 ============================
--- a/OrthancFramework/Sources/Logging.cpp	Tue Dec 09 14:51:25 2025 +0100
+++ b/OrthancFramework/Sources/Logging.cpp	Wed Dec 10 12:01:24 2025 +0100
@@ -34,6 +34,7 @@
 #  include <pthread.h>
 #endif
 
+#include "MetricsRegistry.h"
 
 /*********************************************************
  * Common section
@@ -572,7 +573,8 @@
 static boost::recursive_mutex                   threadNamesMutex_;
 static std::map<boost::thread::id, std::string> threadNames_;
 static bool                                     enableThreadNames_ = true;
-
+static std::list<Orthanc::Logging::ILoggingListener*> loggingListeners_;
+static boost::mutex                                   loggingListenersMutex_;
 
 
 namespace Orthanc
@@ -706,6 +708,18 @@
       return threadNames_[threadId];
     }    
 
+    void AddLoggingListener(ILoggingListener* listener)
+    {
+      boost::mutex::scoped_lock lock(loggingListenersMutex_);
+      loggingListeners_.push_back(listener);
+    }
+
+    void ClearLoggingListeners()
+    {
+      boost::mutex::scoped_lock lock(loggingListenersMutex_);
+      loggingListeners_.clear();
+    }
+
     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
@@ -1048,8 +1062,15 @@
       }
       else if (stream_ != &nullStream_)
       {
-        *stream_ << "\n";
+        *stream_ << messageStream_.str() << "\n";
         stream_->flush();
+
+        boost::mutex::scoped_lock lock(loggingListenersMutex_);
+        for (std::list<Orthanc::Logging::ILoggingListener*>::iterator it = loggingListeners_.begin(); it != loggingListeners_.end(); ++it)
+        {
+          (*it)->HandleLog(level_, category_, pluginName_, file_, line_, messageStream_.str());
+        }
+
       }
     }
       
--- a/OrthancFramework/Sources/Logging.h	Tue Dec 09 14:51:25 2025 +0100
+++ b/OrthancFramework/Sources/Logging.h	Wed Dec 10 12:01:24 2025 +0100
@@ -74,7 +74,20 @@
       LogCategory_JOBS    = (1 << 5),
       LogCategory_LUA     = (1 << 6)
     };
-    
+
+    class ILoggingListener
+    {
+    public:
+      virtual ~ILoggingListener() {}
+
+      virtual void HandleLog(LogLevel level,
+                             LogCategory category,
+                             const std::string& pluginName,
+                             const char* file,
+                             uint32_t line,
+                             const std::string& message) = 0;
+    };
+
     ORTHANC_PUBLIC const char* EnumerationToString(LogLevel level);
 
     ORTHANC_PUBLIC LogLevel StringToLogLevel(const char* level);
@@ -97,6 +110,10 @@
 
     ORTHANC_PUBLIC bool HasCurrentThreadName();
 
+    ORTHANC_PUBLIC void AddLoggingListener(ILoggingListener* listener);
+
+    ORTHANC_PUBLIC void ClearLoggingListeners();
+
     ORTHANC_PUBLIC void EnableThreadNames(bool enabled);
 
     ORTHANC_PUBLIC void EnableInfoLevel(bool enabled);
@@ -270,6 +287,7 @@
       LogCategory                         category_;
       const char*                         file_;
       uint32_t                            line_;
+      std::stringstream                   messageStream_;
 
     public:
       InternalLogger(LogLevel level,
@@ -283,7 +301,7 @@
       template <typename T>
         std::ostream& operator<< (const T& message)
       {
-        return (*stream_) << boost::lexical_cast<std::string>(message);
+        return messageStream_ << boost::lexical_cast<std::string>(message);
       }
     };
 
--- a/OrthancServer/Sources/main.cpp	Tue Dec 09 14:51:25 2025 +0100
+++ b/OrthancServer/Sources/main.cpp	Wed Dec 10 12:01:24 2025 +0100
@@ -1042,6 +1042,37 @@
   return restart;
 }
 
+// This class makes the bridge between the logging and the metrics
+// to count the number of logged errors and warnings
+class MetricsLoggingListener : public Orthanc::Logging::ILoggingListener
+{
+  MetricsRegistry& metrics_;
+public:
+  MetricsLoggingListener(MetricsRegistry& metrics)
+  : metrics_(metrics)
+  {
+  }
+
+  virtual ~MetricsLoggingListener() {};
+
+  virtual void HandleLog(Orthanc::Logging::LogLevel level,
+                         Orthanc::Logging::LogCategory category,
+                         const std::string& pluginName,
+                         const char* file,
+                         uint32_t line,
+                         const std::string& message) ORTHANC_OVERRIDE
+  {
+    if (level == Orthanc::Logging::LogLevel_ERROR)
+    {
+      metrics_.IncrementIntegerValue("orthanc_logged_errors_count", 1);
+    }
+    else if (level == Orthanc::Logging::LogLevel_WARNING)
+    {
+      metrics_.IncrementIntegerValue("orthanc_logged_warnings_count", 1);
+    }
+  }
+
+};
 
 
 static bool StartHttpServer(ServerContext& context,
@@ -1282,8 +1313,13 @@
 
     httpServer.Start();
   
+    MetricsLoggingListener loggingMetrics(context.GetMetricsRegistry());
+    Logging::AddLoggingListener(&loggingMetrics);
+
     bool restart = WaitForExit(context, restApi);
 
+    Logging::ClearLoggingListeners();
+
     httpServer.Stop();
     LOG(WARNING) << "    HTTP server has stopped";
 
--- a/TODO	Tue Dec 09 14:51:25 2025 +0100
+++ b/TODO	Wed Dec 10 12:01:24 2025 +0100
@@ -25,6 +25,7 @@
 * Toolbox::ComputeMD5() fails on files larger than 4GB
 * Logging: add more specific information to contextualize the logs.
   For a DICOM Transfer, that would be nice to include the modality in the context + a study identifier or a job id.
+  Note that the thread id greatly helps already wrt this topic.
 * (1) Accept extra DICOM tags dictionaries in the DCMTK format '.dic' (easier to use than declare
   them in the Orthanc configuration file).  Even the standard dictionaries could be 
   overridden by these custom dictionaries.