changeset 5569:738f80622e91 find-refactoring

merge default -> find-refactoring
author Alain Mazy <am@orthanc.team>
date Thu, 25 Apr 2024 17:07:33 +0200
parents b0b5546f1b9f (current diff) e791a74ea946 (diff)
children 5a13483d12c5
files NEWS OrthancServer/CMakeLists.txt OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp
diffstat 23 files changed, 625 insertions(+), 277 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Apr 25 09:22:07 2024 +0200
+++ b/NEWS	Thu Apr 25 17:07:33 2024 +0200
@@ -10,12 +10,22 @@
   They are now removed from the query earlier to avoid this disk access that 
   could slow down the response time.  Note that this seems to happen mainly 
   when the query originates from some GE devices (AWS).
+* The 0x0111 DIMSE Status is now considered as a warning instead of an error
+  when received as a response to a C-Store. 
+  See https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3
+* Removed potential PHI from the logs when Orthanc encounters an error while
+  creating a zip file.
+
 
 REST API
 --------
 
 * API version upgraded to 24
 * Added "MaximumPatientCount" in /system
+* Added a new "LimitToThisLevelMainDicomTags" field in the payload of 
+  /patients|studies|series/instances/../reconstruct to speed up the reconstruction
+  in case you just want to update the MainDicomTags of that resource level only 
+  e.g. after you have updated the 'ExtraMainDicomTags' for this level.
 * TODO-FIND: complete the list of updated routes:
   /studies?expand and sibbling routes now also return "Metadata"
 
@@ -23,6 +33,13 @@
 -------
 
 * Multitenant DICOM plugin: added support for locales
+* Housekeeper plugin: Added an option "LimitMainDicomTagsReconstructLevel"
+  (allowed values: "Patient", "Study", "Series", "Instance").  This can greatly speed
+  up the housekeeper process e.g. if you have only update the Study level ExtraMainDicomTags.
+* SDK: added OrthancPluginLogMessage that is a new primitive for plugins to log messages.
+  This new primitive will display the plugin name, plugin file name and plugin line number
+  in the logs.  If they are using the OrthancFramework, plugins should now use LOG(INFO),
+  LOG(WARNING) and LOG(ERROR) to log their messages.
 
 
 Version 1.12.3 (2024-01-31)
--- a/OrthancFramework/Sources/Compression/ZipWriter.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancFramework/Sources/Compression/ZipWriter.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -553,7 +553,7 @@
       if (!pimpl_->file_)
       {
         throw OrthancException(ErrorCode_CannotWriteFile,
-                               "Cannot create new ZIP archive: " + path_);
+                               "Cannot create new ZIP archive");  // we do not log the path anymore since it can contain PHI
       }
     }
   }
@@ -632,10 +632,10 @@
                                    compressionLevel_);
     }
 
-    if (result != 0)
+    if (result != ZIP_OK)
     {
       throw OrthancException(ErrorCode_CannotWriteFile,
-                             "Cannot add new file inside ZIP archive: " + std::string(path));
+                             "Cannot add new file inside ZIP archive - error code = " + boost::lexical_cast<std::string>(result)); // we do not log the path anymore since it can contain PHI
     }
 
     hasFileInZip_ = true;
@@ -666,10 +666,11 @@
     {
       int bytes = static_cast<int32_t>(length <= maxBytesInAStep ? length : maxBytesInAStep);
 
-      if (zipWriteInFileInZip(pimpl_->file_, p, bytes))
+      int result = zipWriteInFileInZip(pimpl_->file_, p, bytes);
+      if (result != ZIP_OK)
       {
         throw OrthancException(ErrorCode_CannotWriteFile,
-                               "Cannot write data to ZIP archive: " + path_);
+                               "Cannot write data to ZIP archive - error code =" + boost::lexical_cast<std::string>(result));  // we do not log the path anymore since it can contain PHI
       }
       
       p += bytes;
--- a/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancFramework/Sources/DicomNetworking/DicomStoreUserConnection.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -449,7 +449,8 @@
     if (response.DimseStatus != 0x0000 &&  // Success
         response.DimseStatus != 0xB000 &&  // Warning - Coercion of Data Elements
         response.DimseStatus != 0xB007 &&  // Warning - Data Set does not match SOP Class
-        response.DimseStatus != 0xB006)    // Warning - Elements Discarded
+        response.DimseStatus != 0xB006 &&  // Warning - Elements Discarded
+        response.DimseStatus != 0x0111)    // Warning - Duplicate SOPInstanceUID (https://discourse.orthanc-server.org/t/ignore-dimse-status-0x0111-when-sending-partial-duplicate-studies/4555/3)
     {
       char buf[16];
       sprintf(buf, "%04X", response.DimseStatus);
--- a/OrthancFramework/Sources/Logging.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancFramework/Sources/Logging.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -311,6 +311,10 @@
     {
     }
 
+    void InitializePluginContext(void* pluginContext, const char* pluginName)
+    {
+    }
+
     void Initialize()
     {
     }
@@ -439,6 +443,10 @@
     {
     }
 
+    void InitializePluginContext(void* pluginContext, const char* pluginName)
+    {
+    }
+
     void Initialize()
     {
     }
@@ -488,6 +496,7 @@
     _OrthancPluginService_LogInfo = 1,
     _OrthancPluginService_LogWarning = 2,
     _OrthancPluginService_LogError = 3,
+    _OrthancPluginService_LogMessage = 45,
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
 
@@ -500,6 +509,17 @@
                                    _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;
+
 }
   
 
@@ -539,7 +559,8 @@
 static std::unique_ptr<LoggingStreamsContext>   loggingStreamsContext_;
 static boost::mutex                             loggingStreamsMutex_;
 static Orthanc::Logging::NullStream             nullStream_;
-static OrthancPluginContext*                    pluginContext_ = NULL;
+static OrthancPluginContext*                    pluginContext_ = NULL;    // this is != NULL only when running from a plugin
+static const char*                              pluginName_ = NULL;       // this is != NULL only when running from a plugin
 static boost::recursive_mutex                   threadNamesMutex_;
 static std::map<boost::thread::id, std::string> threadNames_;
 static bool                                     enableThreadNames_ = true;
@@ -667,6 +688,7 @@
 
     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)
@@ -740,7 +762,13 @@
         threadName[0] = '\0';
       }
 
-      prefix = (std::string(date) + threadName + path.filename().string() + ":" +
+      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 &&
@@ -763,6 +791,12 @@
       EnableInfoLevel(true);  // allow the plugin to log at info level (but the Orthanc Core still decides of the level)
     }
 
+    void InitializePluginContext(void* pluginContext, const char* pluginName)
+    {
+      InitializePluginContext(pluginContext);
+      pluginName_ = pluginName;
+    }
+
 
     void Initialize()
     {
@@ -837,6 +871,7 @@
 
 
     void InternalLogger::Setup(LogCategory category,
+                               const char* pluginName,
                                const char* file,
                                int line)
     {
@@ -869,7 +904,7 @@
         }
 
         std::string prefix;
-        GetLinePrefix(prefix, level_, file, line, category);
+        GetLinePrefix(prefix, level_, pluginName, file, line, category);
 
         {
           // We lock the global mutex. The mutex is locked until the
@@ -878,7 +913,7 @@
       
           if (loggingStreamsContext_.get() == NULL)
           {
-            fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine\n");
+            fprintf(stderr, "ERROR: Trying to log a message after the finalization of the logging engine (or did you forgot to initialize it ?)\n"); // have you called Orthanc::Logging::InitializePluginContext ?
             lock_.unlock();
             return;
           }
@@ -934,20 +969,42 @@
                                    int line) :
       lock_(loggingStreamsMutex_, boost::defer_lock_t()),
       level_(level),
-      stream_(&nullStream_)  // By default, logging to "/dev/null" is simulated
+      stream_(&nullStream_),  // By default, logging to "/dev/null" is simulated
+      category_(category),
+      file_(file),
+      line_(line)
     {
-      Setup(category, file, line);
+      Setup(category, NULL, file, line);
     }
 
+    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)
+    {
+      Setup(category, pluginName, file, line);
+    }
+
+
 
     InternalLogger::InternalLogger(LogLevel level,
                                    const char* file,
                                    int line) :
       lock_(loggingStreamsMutex_, boost::defer_lock_t()),
       level_(level),
-      stream_(&nullStream_)  // By default, logging to "/dev/null" is simulated
+      stream_(&nullStream_),  // By default, logging to "/dev/null" is simulated
+      category_(LogCategory_GENERIC),
+      file_(file),
+      line_(line)
     {
-      Setup(LogCategory_GENERIC, file, line);
+      Setup(LogCategory_GENERIC, NULL, file, line);
     }
 
 
@@ -961,22 +1018,36 @@
 
         if (pluginContext_ != NULL)
         {
-          switch (level_)
+          if (pluginName_ != NULL) // this shall happen only if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4) && ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
           {
-            case LogLevel_ERROR:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogError, message.c_str());
-              break;
+            _OrthancPluginLogMessage m;
+            m.category = category_;
+            m.level = level_;
+            m.file = file_;
+            m.line = line_;
+            m.plugin = pluginName_;
+            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_WARNING:
+                pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogWarning, message.c_str());
+                break;
 
-            case LogLevel_INFO:
-              pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
-              break;
+              case LogLevel_INFO:
+                pluginContext_->InvokeService(pluginContext_, _OrthancPluginService_LogInfo, message.c_str());
+                break;
 
-            default:
-              break;
+              default:
+                break;
+            }
           }
         }
       }
--- a/OrthancFramework/Sources/Logging.h	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancFramework/Sources/Logging.h	Thu Apr 25 17:07:33 2024 +0200
@@ -46,12 +46,13 @@
 {
   namespace Logging
   {
+    // Note: these values must match the ones in OrthancCPlugin.h
     enum LogLevel
     {
-      LogLevel_ERROR,
-      LogLevel_WARNING,
-      LogLevel_INFO,
-      LogLevel_TRACE
+      LogLevel_ERROR     = 0,
+      LogLevel_WARNING   = 1,
+      LogLevel_INFO      = 2,
+      LogLevel_TRACE     = 3
     };
 
     /**
@@ -59,6 +60,7 @@
      * mask. As a consequence, there can be up to 31 log categories
      * (not 32, as the value GENERIC is reserved for the log commands
      * that don't fall in a specific category).
+     * Note: these values must match the ones in OrthancCPlugin.h
      **/
     enum LogCategory
     {
@@ -78,6 +80,9 @@
     // "pluginContext" must be of type "OrthancPluginContext"
     ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext);
 
+    // note: this variant shall be called only from a plugin and only if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4) && ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4)
+    ORTHANC_PUBLIC void InitializePluginContext(void* pluginContext, const char* pluginName);
+
     ORTHANC_PUBLIC void Initialize();
 
     ORTHANC_PUBLIC void Finalize();
@@ -162,6 +167,7 @@
 #  define LOG(level)            ::Orthanc::Logging::NullStream()
 #  define VLOG(unused)          ::Orthanc::Logging::NullStream()
 #  define CLOG(level, category) ::Orthanc::Logging::NullStream()
+#  define LOG_FROM_PLUGIN(level, category, pluginName, file, line)  ::Orthanc::Logging::NullStream()
 #else /* ORTHANC_ENABLE_LOGGING == 1 */
 
 #if !defined(__ORTHANC_FILE__)
@@ -182,6 +188,8 @@
 #  define CLOG(level, category) ::Orthanc::Logging::InternalLogger      \
   (::Orthanc::Logging::LogLevel_ ## level,                              \
    ::Orthanc::Logging::LogCategory_ ## category, __ORTHANC_FILE__, __LINE__)
+#  define LOG_FROM_PLUGIN(level, category, pluginName, file, line)  ::Orthanc::Logging::InternalLogger      \
+  (level, category, pluginName, file, line)
 #endif
 
 
@@ -258,8 +266,12 @@
       LogLevel                            level_;
       std::unique_ptr<std::stringstream>  pluginStream_;
       std::ostream*                       stream_;
+      LogCategory                         category_;
+      const char*                         file_;
+      uint32_t                            line_;
 
       void Setup(LogCategory category,
+                 const char* pluginName,
                  const char* file,
                  int line);
 
@@ -269,6 +281,12 @@
                      const char* file,
                      int line);
 
+      InternalLogger(LogLevel level,
+                     LogCategory category,
+                     const char* pluginName,
+                     const char* file,
+                     int line);
+
       // For backward binary compatibility with Orthanc Framework <= 1.8.0
       InternalLogger(LogLevel level,
                      const char* file,
--- a/OrthancServer/CMakeLists.txt	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/CMakeLists.txt	Thu Apr 25 17:07:33 2024 +0200
@@ -500,6 +500,10 @@
     
     ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/ThirdParty/base64/base64.cpp
     ${CMAKE_SOURCE_DIR}/../OrthancFramework/Resources/ThirdParty/md5/md5.c
+
+    # the orthanc framework sources
+    ${ORTHANC_CORE_SOURCES}
+
     Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
     )
 
@@ -524,6 +528,8 @@
 
   DefineSourceBasenameForTarget(PluginsDependencies)
 
+  target_include_directories(PluginsDependencies PUBLIC ${CMAKE_SOURCE_DIR}/../OrthancFramework/Sources)
+
   # Add the "-fPIC" option as this static library must be embedded
   # inside shared libraries (important on UNIX)
   set_target_properties(
--- a/OrthancServer/Plugins/Engine/PluginsManager.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsManager.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -161,6 +161,18 @@
         CLOG(INFO, PLUGINS) << reinterpret_cast<const char*>(params);
         return OrthancPluginErrorCode_Success;
 
+      case _OrthancPluginService_LogMessage:
+        {
+          const _OrthancPluginLogMessage& m = *reinterpret_cast<const _OrthancPluginLogMessage*>(params);
+          // we may convert directly from OrthancPluginLogLevel to LogLevel (and category) because the enum values must be identical 
+          // for Orthanc::Logging to work both in the core and in the plugins
+          Orthanc::Logging::LogLevel level = static_cast<Orthanc::Logging::LogLevel>(m.level);
+          Orthanc::Logging::LogCategory category = static_cast<Orthanc::Logging::LogCategory>(m.category);
+          
+          LOG_FROM_PLUGIN(level, category, m.plugin, m.file, m.line) << m.message;
+          return OrthancPluginErrorCode_Success;
+        };
+
       default:
         break;
     }
--- a/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Include/orthanc/OrthancCPlugin.h	Thu Apr 25 17:07:33 2024 +0200
@@ -465,6 +465,7 @@
     _OrthancPluginService_GetDatabaseServerIdentifier = 42,         /* New in Orthanc 1.11.1 */
     _OrthancPluginService_SetMetricsIntegerValue = 43,              /* New in Orthanc 1.12.1 */
     _OrthancPluginService_SetCurrentThreadName = 44,                /* New in Orthanc 1.12.2 */
+    _OrthancPluginService_LogMessage = 45,                          /* New in Orthanc 1.12.4 */
 
 
     /* Registration of callbacks */
@@ -9479,6 +9480,79 @@
     return context->InvokeService(context, _OrthancPluginService_SetCurrentThreadName, threadName);
   }
 
+  typedef enum
+  {
+    // these values must match LogLevel in the Orthanc Core
+    OrthancPluginLogLevel_Error = 0,    /*!< Error log level */
+    OrthancPluginLogLevel_Warning = 1,  /*!< Warning log level */
+    OrthancPluginLogLevel_Info = 2,     /*!< Info log level */
+    OrthancPluginLogLevel_Trace = 3,    /*!< Trace log level */
+  
+    // Force the enum to be 32 bits
+    OrthancPluginLogLevel_INTERNAL = 0x7FFFFFFF
+  } OrthancPluginLogLevel;
+
+  typedef enum
+  {
+    // these values must match LogCategory in the Orthanc Core
+    OrthancPluginLogCategory_Generic = (1 << 0),  /*!< Generic (default) category */
+    OrthancPluginLogCategory_Plugins = (1 << 1),  /*!< Plugin engine related logs (shall not be used by plugins) */
+    OrthancPluginLogCategory_Http    = (1 << 2),  /*!< HTTP related logs */
+    OrthancPluginLogCategory_Sqlite  = (1 << 3),  /*!< SQLite related logs (shall not be used by plugins) */
+    OrthancPluginLogCategory_Dicom   = (1 << 4),  /*!< DICOM related logs */
+    OrthancPluginLogCategory_Jobs    = (1 << 5),  /*!< jobs related logs */
+    OrthancPluginLogCategory_Lua     = (1 << 6),  /*!< Lua related logs (shall not be used by plugins) */
+
+    // Force the enum to be 32 bits
+    OrthancPluginLogCategory_INTERNAL = 0x7FFFFFFF
+  } OrthancPluginLogCategory;
+
+
+  // note: this structure is also defined in Logging.h and it must be binary compatible
+  typedef struct
+  {
+    const char*               message;
+    const char*               plugin;
+    const char*               file;
+    uint32_t                  line;
+    OrthancPluginLogCategory  category;
+    OrthancPluginLogLevel     level;
+  } _OrthancPluginLogMessage;
+
+
+  /**
+   * @brief Log a message.
+   *
+   * Log a message using the Orthanc logging system.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param message The message to be logged.
+   * @param plugin The plugin name.
+   * @param file The filename in the plugin code.
+   * @param line The file line in the plugin code.
+   * @param category The category.
+   * @param level The level of the message.
+   **/
+  ORTHANC_PLUGIN_INLINE void OrthancPluginLogMessage(
+    OrthancPluginContext* context,
+    const char* message,
+    const char* plugin,
+    const char* file,
+    uint32_t line,
+    OrthancPluginLogCategory category,
+    OrthancPluginLogLevel level)
+  {
+    _OrthancPluginLogMessage m;
+    m.message = message;
+    m.plugin = plugin;
+    m.file = file;
+    m.category = category;
+    m.line = line;
+    m.level = level;
+    context->InvokeService(context, _OrthancPluginService_LogMessage, &m);
+  }
+
+
 #ifdef  __cplusplus
 }
 #endif
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -79,9 +79,20 @@
     }
   }
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) && ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4)
+  static const char* pluginName_ = NULL;
+
+  void SetGlobalContext(OrthancPluginContext* context, const char* pluginName)
+  {
+    SetGlobalContext(context);
+    pluginName_ = pluginName;
+  }
+#endif
+
   void ResetGlobalContext()
   {
     globalContext_ = NULL;
+    pluginName_ = NULL;
   }
 
   bool HasGlobalContext()
@@ -102,6 +113,54 @@
     }
   }
 
+  void LogError(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogError(GetGlobalContext(), message.c_str());
+    }
+  }
+
+  void LogWarning(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
+    }
+  }
+
+  void LogInfo(const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
+    }
+  }
+
+
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) && ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4)
+  // This file does not have any dependencies on Logging.h, we must call the "native" plugin service
+
+  void _LogMessage(OrthancPluginLogLevel level, const char* file, uint32_t line, const std::string& message)
+  {
+    if (HasGlobalContext())
+    {
+      OrthancPluginLogMessage(GetGlobalContext(), message.c_str(), pluginName_, file, line, OrthancPluginLogCategory_Generic, level);
+    }
+  }
+
+  #define LOG_ERROR(msg) _LogMessage(OrthancPluginLogLevel_Error, __ORTHANC_FILE__, __LINE__, msg);
+  #define LOG_WARNING(msg) _LogMessage(OrthancPluginLogLevel_Warning, __ORTHANC_FILE__, __LINE__, msg);
+  #define LOG_INFO(msg) _LogMessage(OrthancPluginLogLevel_Info, __ORTHANC_FILE__, __LINE__, msg);
+
+#else
+
+  #define LOG_ERROR(msg) LogError(msg);
+  #define LOG_WARNING(msg) LogWarning(msg);
+  #define LOG_INFO(msg) LogInfo(msg);
+
+#endif
+
 
   void MemoryBuffer::Check(OrthancPluginErrorCode code)
   {
@@ -233,7 +292,7 @@
 
     if (!ReadJson(target, buffer_.data, buffer_.size))
     {
-      LogError("Cannot convert some memory buffer to JSON");
+      LOG_ERROR("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -404,7 +463,7 @@
     }
     else
     {
-      LogError("Cannot parse JSON: " + std::string(err));
+      LOG_ERROR("Cannot parse JSON: " + std::string(err));
       return false;
     }
 #endif
@@ -565,13 +624,13 @@
   {
     if (str_ == NULL)
     {
-      LogError("Cannot convert an empty memory buffer to JSON");
+      LOG_ERROR("Cannot convert an empty memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     if (!ReadJson(target, str_))
     {
-      LogError("Cannot convert some memory buffer to JSON");
+      LOG_ERROR("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -581,13 +640,13 @@
   {
     if (str_ == NULL)
     {
-      LogError("Cannot convert an empty memory buffer to JSON");
+      LOG_ERROR("Cannot convert an empty memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
     if (!ReadJsonWithoutComments(target, str_))
     {
-      LogError("Cannot convert some memory buffer to JSON");
+      LOG_ERROR("Cannot convert some memory buffer to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
@@ -625,7 +684,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -645,7 +704,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -688,34 +747,6 @@
     }
   }
 
-
-  void LogError(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogError(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void LogWarning(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogWarning(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
-  void LogInfo(const std::string& message)
-  {
-    if (HasGlobalContext())
-    {
-      OrthancPluginLogInfo(GetGlobalContext(), message.c_str());
-    }
-  }
-
-
   void OrthancConfiguration::LoadConfiguration()
   {
     OrthancString str;
@@ -723,7 +754,7 @@
 
     if (str.GetContent() == NULL)
     {
-      LogError("Cannot access the Orthanc configuration");
+      LOG_ERROR("Cannot access the Orthanc configuration");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -731,7 +762,7 @@
 
     if (configuration_.type() != Json::objectValue)
     {
-      LogError("Unable to read the Orthanc configuration");
+      LOG_ERROR("Unable to read the Orthanc configuration");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
@@ -799,8 +830,8 @@
     {
       if (configuration_[key].type() != Json::objectValue)
       {
-        LogError("The configuration section \"" + target.path_ +
-                 "\" is not an associative array as expected");
+        LOG_ERROR("The configuration section \"" + target.path_ +
+                  "\" is not an associative array as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
       }
@@ -822,8 +853,8 @@
 
     if (configuration_[key].type() != Json::stringValue)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a string as expected");
+      LOG_ERROR("The configuration option \"" + GetPath(key) +
+                "\" is not a string as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -854,8 +885,8 @@
         return true;
 
       default:
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not an integer as expected");
+        LOG_ERROR("The configuration option \"" + GetPath(key) +
+                  "\" is not an integer as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -873,8 +904,8 @@
 
     if (tmp < 0)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a positive integer as expected");
+      LOG_ERROR("The configuration option \"" + GetPath(key) +
+                "\" is not a positive integer as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -898,8 +929,8 @@
 
     if (configuration_[key].type() != Json::booleanValue)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not a Boolean as expected");
+      LOG_ERROR("The configuration option \"" + GetPath(key) +
+                "\" is not a Boolean as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -934,8 +965,8 @@
         return true;
 
       default:
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not an integer as expected");
+        LOG_ERROR("The configuration option \"" + GetPath(key) +
+                  "\" is not an integer as expected");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -994,8 +1025,8 @@
         break;
     }
 
-    LogError("The configuration option \"" + GetPath(key) +
-             "\" is not a list of strings as expected");
+    LOG_ERROR("The configuration option \"" + GetPath(key) +
+              "\" is not a list of strings as expected");
 
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
@@ -1115,8 +1146,8 @@
 
     if (configuration_[key].type() != Json::objectValue)
     {
-      LogError("The configuration option \"" + GetPath(key) +
-               "\" is not an object as expected");
+      LOG_ERROR("The configuration option \"" + GetPath(key) +
+                "\" is not an object as expected");
 
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
@@ -1133,8 +1164,8 @@
       }
       else
       {
-        LogError("The configuration option \"" + GetPath(key) +
-                 "\" is not a dictionary mapping strings to strings");
+        LOG_ERROR("The configuration option \"" + GetPath(key) +
+                  "\" is not a dictionary mapping strings to strings");
 
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
       }
@@ -1156,7 +1187,7 @@
   {
     if (image_ == NULL)
     {
-      LogError("Trying to access a NULL image");
+      LOG_ERROR("Trying to access a NULL image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1182,7 +1213,7 @@
 
     if (image_ == NULL)
     {
-      LogError("Cannot create an image");
+      LOG_ERROR("Cannot create an image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
@@ -1199,7 +1230,7 @@
 
     if (image_ == NULL)
     {
-      LogError("Cannot create an image accessor");
+      LOG_ERROR("Cannot create an image accessor");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
   }
@@ -1213,7 +1244,7 @@
 
     if (image_ == NULL)
     {
-      LogError("Cannot uncompress a PNG image");
+      LOG_ERROR("Cannot uncompress a PNG image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1226,7 +1257,7 @@
     image_ = OrthancPluginUncompressImage(GetGlobalContext(), data, size, OrthancPluginImageFormat_Jpeg);
     if (image_ == NULL)
     {
-      LogError("Cannot uncompress a JPEG image");
+      LOG_ERROR("Cannot uncompress a JPEG image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1240,7 +1271,7 @@
     image_ = OrthancPluginDecodeDicomImage(GetGlobalContext(), data, size, frame);
     if (image_ == NULL)
     {
-      LogError("Cannot uncompress a DICOM image");
+      LOG_ERROR("Cannot uncompress a DICOM image");
       ORTHANC_PLUGINS_THROW_EXCEPTION(ParameterOutOfRange);
     }
   }
@@ -1654,13 +1685,13 @@
                                    unsigned int minor,
                                    unsigned int revision)
   {
-    LogError("Your version of the Orthanc core (" +
-             std::string(GetGlobalContext()->orthancVersion) +
-             ") is too old to run this plugin (version " +
-             boost::lexical_cast<std::string>(major) + "." +
-             boost::lexical_cast<std::string>(minor) + "." +
-             boost::lexical_cast<std::string>(revision) +
-             " is required)");
+    LOG_ERROR("Your version of the Orthanc core (" +
+              std::string(GetGlobalContext()->orthancVersion) +
+              ") is too old to run this plugin (version " +
+              boost::lexical_cast<std::string>(major) + "." +
+              boost::lexical_cast<std::string>(minor) + "." +
+              boost::lexical_cast<std::string>(revision) +
+              " is required)");
   }
 
   bool CheckMinimalVersion(const char* version,
@@ -1740,7 +1771,7 @@
   {
     if (!HasGlobalContext())
     {
-      LogError("Bad Orthanc context in the plugin");
+      LOG_ERROR("Bad Orthanc context in the plugin");
       return false;
     }
 
@@ -1777,7 +1808,7 @@
     }
     else
     {
-      LogError("Inexistent peer: " + name);
+      LOG_ERROR("Inexistent peer: " + name);
       ORTHANC_PLUGINS_THROW_EXCEPTION(UnknownResource);
     }
   }
@@ -2061,7 +2092,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -2098,7 +2129,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -2464,7 +2495,7 @@
 
     if (id == NULL)
     {
-      LogError("Plugin cannot submit job");
+      LOG_ERROR("Plugin cannot submit job");
       OrthancPluginFreeJob(GetGlobalContext(), orthanc);
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_Plugin);
     }
@@ -2533,7 +2564,7 @@
           throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()),
                                           status["ErrorDescription"].asString());
 #else
-          LogError("Exception while executing the job: " + status["ErrorDescription"].asString());
+          LOG_ERROR("Exception while executing the job: " + status["ErrorDescription"].asString());
           ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());          
 #endif
         }
@@ -2558,7 +2589,7 @@
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat,
                                       "Expected a JSON object in the body");
 #else
-      LogError("Expected a JSON object in the body");
+      LOG_ERROR("Expected a JSON object in the body");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
     }
@@ -2574,7 +2605,7 @@
                                         "Option \"" + std::string(KEY_SYNCHRONOUS) +
                                         "\" must be Boolean");
 #else
-        LogError("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean");
+        LOG_ERROR("Option \"" + std::string(KEY_SYNCHRONOUS) + "\" must be Boolean");
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
       }
@@ -2593,7 +2624,7 @@
                                         "Option \"" + std::string(KEY_ASYNCHRONOUS) +
                                         "\" must be Boolean");
 #else
-        LogError("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean");
+        LOG_ERROR("Option \"" + std::string(KEY_ASYNCHRONOUS) + "\" must be Boolean");
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
       }
@@ -2614,7 +2645,7 @@
                                         "Option \"" + std::string(KEY_PRIORITY) +
                                         "\" must be an integer");
 #else
-        LogError("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer");
+        LOG_ERROR("Option \"" + std::string(KEY_PRIORITY) + "\" must be an integer");
         ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
 #endif
       }
@@ -3135,7 +3166,7 @@
 
     if (body.size() > 0xffffffffu)
     {
-      LogError("Cannot handle body size > 4GB");
+      LOG_ERROR("Cannot handle body size > 4GB");
       ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
     }
 
@@ -3280,7 +3311,7 @@
     
     if (!ReadJson(answerBody, body))
     {
-      LogError("Cannot convert HTTP answer body to JSON");
+      LOG_ERROR("Cannot convert HTTP answer body to JSON");
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
   }
--- a/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Thu Apr 25 17:07:33 2024 +0200
@@ -137,6 +137,10 @@
 
   void SetGlobalContext(OrthancPluginContext* context);
 
+#if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 12, 4) && ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 12, 4)
+  void SetGlobalContext(OrthancPluginContext* context, const char* pluginName);
+#endif
+
   void ResetGlobalContext();
 
   bool HasGlobalContext();
@@ -637,11 +641,11 @@
   const char* AutodetectMimeType(const std::string& path);
 #endif
 
-  void LogError(const std::string& message);
+  void LogError(const std::string& message);   // From Orthanc 1.12.4, use LOG(ERROR) to display the plugin name, file and line (First set a plugin name in Orthanc::Logging::InitializePluginContext)
 
-  void LogWarning(const std::string& message);
+  void LogWarning(const std::string& message); // From Orthanc 1.12.4, use LOG(WARNING) to display the plugin name, file and line (First set a plugin name in Orthanc::Logging::InitializePluginContext)
 
-  void LogInfo(const std::string& message);
+  void LogInfo(const std::string& message);    // From Orthanc 1.12.4, use LOG(INFO) to display the plugin name, file and line (First set a plugin name in Orthanc::Logging::InitializePluginContext)
 
   void ReportMinimalOrthancVersion(unsigned int major,
                                    unsigned int minor,
--- a/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/DelayedDeletion/Plugin.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -117,7 +117,7 @@
     // copy from a buffer allocated on plugin's heap into a buffer allocated on core's heap
     if (OrthancPluginCreateMemoryBuffer64(OrthancPlugins::GetGlobalContext(), target, buffer->GetSize()) != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError("Delayed deletion plugin: error while reading object " + std::string(uuid) + ", cannot allocate memory of size " + boost::lexical_cast<std::string>(buffer->GetSize()) + " bytes");
+      LOG(ERROR) << "Delayed deletion plugin: error while reading object " << uuid << ", cannot allocate memory of size " << buffer->GetSize() << " bytes";
       return OrthancPluginErrorCode_StorageAreaPlugin;
     }
 
@@ -289,8 +289,8 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    OrthancPlugins::SetGlobalContext(context);
-    Orthanc::Logging::InitializePluginContext(context);
+    OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME);
+    Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME);
     
 
     /* Check the version of the Orthanc core */
--- a/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/Housekeeper/Plugin.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -23,6 +23,7 @@
 #define HOUSEKEEPER_NAME "housekeeper"
 
 #include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
 #include "../Common/OrthancPluginCppWrapper.h"
 
 #include <boost/thread.hpp>
@@ -48,6 +49,10 @@
 static bool triggerOnUnnecessaryDicomAsJsonFiles_ = true;
 static bool triggerOnIngestTranscodingChange_ = true;
 static bool triggerOnDicomWebCacheChange_ = true;
+static std::string limitMainDicomTagsReconstructLevel_ = "";
+static std::string limitToChange_ = "";
+static std::string limitToUrl_ = "";
+
 
 struct RunningPeriod
 {
@@ -87,7 +92,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: invalid schedule: unknown 'day': " + weekday);      
+      LOG(WARNING) << "Housekeeper: invalid schedule: unknown 'day': " << weekday;
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -399,12 +404,12 @@
   {
     if (triggerOnUnnecessaryDicomAsJsonFiles_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: your storage might still contain some dicom-as-json files -> will perform housekeeping";
       needsReconstruct = true;  // the default reconstruct removes the dicom-as-json
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled");
+      LOG(WARNING) << "Housekeeper: your storage might still contain some dicom-as-json files but the trigger has been disabled";
     }
   }
 
@@ -412,12 +417,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: Patient main dicom tags have changed, -> will perform housekeeping";
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Patient main dicom tags have changed but the trigger is disabled");
+      LOG(WARNING) << "Housekeeper: Patient main dicom tags have changed but the trigger is disabled";
     }
   }
 
@@ -425,12 +430,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed, -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: Study main dicom tags have changed, -> will perform housekeeping";
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Study main dicom tags have changed but the trigger is disabled");
+      LOG(WARNING) << "Housekeeper: Study main dicom tags have changed but the trigger is disabled";
     }
   }
 
@@ -438,12 +443,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed, -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: Series main dicom tags have changed, -> will perform housekeeping";
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Series main dicom tags have changed but the trigger is disabled");
+      LOG(WARNING) << "Housekeeper: Series main dicom tags have changed but the trigger is disabled";
     }
   }
 
@@ -451,12 +456,12 @@
   {
     if (triggerOnMainDicomTagsChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: Instance main dicom tags have changed, -> will perform housekeeping";
       needsReconstruct = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: Instance main dicom tags have changed but the trigger is disabled");
+      LOG(WARNING) << "Housekeeper: Instance main dicom tags have changed but the trigger is disabled";
     }
   }
 
@@ -466,18 +471,18 @@
     {
       if (current.storageCompressionEnabled)
       {
-        OrthancPlugins::LogWarning("Housekeeper: storage compression is now enabled -> will perform housekeeping");
+        LOG(WARNING) << "Housekeeper: storage compression is now enabled -> will perform housekeeping";
       }
       else
       {
-        OrthancPlugins::LogWarning("Housekeeper: storage compression is now disabled -> will perform housekeeping");
+        LOG(WARNING) << "Housekeeper: storage compression is now disabled -> will perform housekeeping";
       }
       
       needsReingest = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: storage compression has changed but the trigger is disabled");
+      LOG(WARNING) << "Housekeeper: storage compression has changed but the trigger is disabled";
     }
   }
 
@@ -485,13 +490,13 @@
   {
     if (triggerOnIngestTranscodingChange_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: ingest transcoding has changed -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: ingest transcoding has changed -> will perform housekeeping";
       
       needsReingest = true;
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: ingest transcoding has changed but the trigger is disabled");
+      LOG(WARNING) << "Housekeeper: ingest transcoding has changed but the trigger is disabled";
     }
   }
 
@@ -501,7 +506,7 @@
     {
       if (triggerOnDicomWebCacheChange_)
       {
-        OrthancPlugins::LogWarning("Housekeeper: DicomWEB plugin is enabled and the housekeeper has never run, you might miss series metadata cache -> will perform housekeeping");
+        LOG(WARNING) << "Housekeeper: DicomWEB plugin is enabled and the housekeeper has never run, you might miss series metadata cache -> will perform housekeeping";
       }
       needsDicomWebCaching = triggerOnDicomWebCacheChange_;
     }
@@ -513,12 +518,12 @@
       {
         if (triggerOnDicomWebCacheChange_)
         {
-          OrthancPlugins::LogWarning("Housekeeper: DicomWEB plugin might miss series metadata cache -> will perform housekeeping");
+          LOG(WARNING) << "Housekeeper: DicomWEB plugin might miss series metadata cache -> will perform housekeeping";
           needsDicomWebCaching = true;
         }
         else
         {
-          OrthancPlugins::LogWarning("Housekeeper: DicomWEB plugin might miss series metadata cache but the trigger has been disabled");
+          LOG(WARNING) << "Housekeeper: DicomWEB plugin might miss series metadata cache but the trigger has been disabled";
         }
       }
     }
@@ -544,24 +549,38 @@
       const Json::Value& change = changes["Changes"][i];
       int64_t seq = change["Seq"].asInt64();
 
-      if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
+      if (!limitToChange_.empty()) // if updating only maindicomtags for a single level 
       {
-        Json::Value result;
+        if (change["ChangeType"] == limitToChange_)
+        {
+          Json::Value result;
+          Json::Value request;
+          request["ReconstructFiles"] = false;
+          request["LimitToThisLevelMainDicomTags"] = true;
+          OrthancPlugins::RestApiPost(result, "/" + limitToUrl_ + "/" + change["ID"].asString() + "/reconstruct", request, false);
+        }
+      }
+      else
+      {
+        if (change["ChangeType"] == "NewStudy") // some StableStudy might be missing if orthanc was shutdown during a StableAge -> consider only the NewStudy events that can not be missed
+        {
+          Json::Value result;
 
-        if (needsReconstruct)
-        {
-          Json::Value request;
-          if (needsReingest)
+          if (needsReconstruct)
           {
-            request["ReconstructFiles"] = true;
+            Json::Value request;
+            if (needsReingest)
+            {
+              request["ReconstructFiles"] = true;
+            }
+            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
           }
-          OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/reconstruct", request, false);
-        }
 
-        if (needsDicomWebCaching)
-        {
-          Json::Value request;
-          OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
+          if (needsDicomWebCaching)
+          {
+            Json::Value request;
+            OrthancPlugins::RestApiPost(result, "/studies/" + change["ID"].asString() + "/update-dicomweb-cache", request, true);
+          }
         }
       }
 
@@ -641,7 +660,7 @@
 
   if (!needsProcessing)
   {
-    OrthancPlugins::LogWarning("Housekeeper: everything has been processed already !");
+    LOG(WARNING) << "Housekeeper: everything has been processed already !";
     return;
   }
 
@@ -649,11 +668,11 @@
   {
     if (force_)
     {
-      OrthancPlugins::LogWarning("Housekeeper: forcing execution -> will perform housekeeping");
+      LOG(WARNING) << "Housekeeper: forcing execution -> will perform housekeeping";
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !");
+      LOG(WARNING) << "Housekeeper: the DB configuration has changed since last run, will reprocess the whole DB !";
     }
     
     Json::Value changes;
@@ -669,7 +688,7 @@
   }
   else
   {
-    OrthancPlugins::LogWarning("Housekeeper: the DB configuration has not changed since last run, will continue processing changes");
+    LOG(WARNING) << "Housekeeper: the DB configuration has not changed since last run, will continue processing changes";
   }
 
   bool completed = false;
@@ -691,9 +710,7 @@
       {
         boost::recursive_mutex::scoped_lock lock(pluginStatusMutex_);
     
-        OrthancPlugins::LogInfo("Housekeeper: processed changes " + 
-                                boost::lexical_cast<std::string>(pluginStatus_.lastProcessedChange) + 
-                                " / " + boost::lexical_cast<std::string>(pluginStatus_.lastChangeToProcess));
+        LOG(INFO) << "Housekeeper: processed changes " << pluginStatus_.lastProcessedChange << " / " << pluginStatus_.lastChangeToProcess;
         
         boost::this_thread::sleep(boost::posix_time::milliseconds(throttleDelay_ * 100));  // wait 1/10 of the delay between changes
       }
@@ -704,7 +721,7 @@
     {
       if (!loggedNotRightPeriodChangeMessage)
       {
-        OrthancPlugins::LogInfo("Housekeeper: entering quiet period");
+        LOG(INFO) << "Housekeeper: entering quiet period";
         loggedNotRightPeriodChangeMessage = true;
       }
 
@@ -778,7 +795,8 @@
 
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
   {
-    OrthancPlugins::SetGlobalContext(c);
+    OrthancPlugins::SetGlobalContext(c, HOUSEKEEPER_NAME);
+    Orthanc::Logging::InitializePluginContext(c, HOUSEKEEPER_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
@@ -789,7 +807,7 @@
       return -1;
     }
 
-    OrthancPlugins::LogWarning("Housekeeper plugin is initializing");
+    LOG(WARNING) << "Housekeeper plugin is initializing";
     OrthancPluginSetDescription2(c, HOUSEKEEPER_NAME, "Optimizes your DB and storage.");
 
     OrthancPlugins::OrthancConfiguration orthancConfiguration;
@@ -842,7 +860,11 @@
               "MainDicomTagsChange": true,
               "UnnecessaryDicomAsJsonFiles": true,
               "DicomWebCacheChange": true   // new in 1.12.2
-            }
+            },
+
+            // When rebuilding MainDicomTags, limit to a single level of resource.
+            // Allowed values: "Patient", "Study", "Series", "Instance"
+            "LimitMainDicomTagsReconstructLevel": "Study"
 
           }
         }
@@ -865,6 +887,33 @@
         triggerOnDicomWebCacheChange_ = triggers.GetBooleanValue("DicomWebCacheChange", true);
       }
 
+      limitMainDicomTagsReconstructLevel_ = housekeeper.GetStringValue("LimitMainDicomTagsReconstructLevel", "");
+      if (limitMainDicomTagsReconstructLevel_ != "Patient" && limitMainDicomTagsReconstructLevel_ != "Study"
+        && limitMainDicomTagsReconstructLevel_ != "Series" && limitMainDicomTagsReconstructLevel_ != "Instance")
+      {
+        LOG(ERROR) << "Housekeeper invalid value for 'LimitMainDicomTagsReconstructLevel': '" << limitMainDicomTagsReconstructLevel_ << "'";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Patient")
+      {
+        limitToChange_ = "NewPatient";
+        limitToUrl_ = "patients";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Study")
+      {
+        limitToChange_ = "NewStudy";
+        limitToUrl_ = "studies";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Series")
+      {
+        limitToChange_ = "NewSeries";
+        limitToUrl_ = "series";
+      }
+      else if (limitMainDicomTagsReconstructLevel_ == "Instance")
+      {
+        limitToChange_ = "NewInstance";
+        limitToUrl_ = "instances";
+      }
+
       if (housekeeper.GetJson().isMember("Schedule"))
       {
         runningPeriods_.load(housekeeper.GetJson()["Schedule"]);
@@ -876,7 +925,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning("Housekeeper plugin is disabled by the configuration file");
+      LOG(WARNING) << "Housekeeper plugin is disabled by the configuration file";
     }
 
     return 0;
@@ -885,7 +934,7 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPlugins::LogWarning("Housekeeper plugin is finalizing");
+    LOG(WARNING) << "Housekeeper plugin is finalizing";
   }
 
 
--- a/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ModalityWorklists/Plugin.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -23,6 +23,7 @@
 #define MODALITY_WORKLISTS_NAME "worklists"
 
 #include "../../../../OrthancFramework/Sources/Compatibility.h"
+#include "../../../../OrthancFramework/Sources/Logging.h"
 #include "../Common/OrthancPluginCppWrapper.h"
 
 #include <boost/filesystem.hpp>
@@ -54,7 +55,7 @@
 
     if (code != OrthancPluginErrorCode_Success)
     {
-      OrthancPlugins::LogError("Error while adding an answer to a worklist request");
+      LOG(ERROR) << "Error while adding an answer to a worklist request";
       ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code);
     }
 
@@ -77,8 +78,7 @@
   dicom.DicomToJson(json, OrthancPluginDicomToJsonFormat_Short,
                     static_cast<OrthancPluginDicomToJsonFlags>(0), 0);
 
-  OrthancPlugins::LogInfo("Received worklist query from remote modality " +
-                          std::string(issuerAet) + ":\n" + json.toStyledString());
+  LOG(INFO) << "Received worklist query from remote modality " << issuerAet << ":\n" + json.toStyledString();
 
   if (!filterIssuerAet_)
   {
@@ -185,21 +185,19 @@
                 return OrthancPluginErrorCode_Success;
               }
               
-              OrthancPlugins::LogInfo("Worklist matched: " + it->path().string());
+              LOG(INFO) << "Worklist matched: " << it->path().string();
               matchedWorklistCount++;
             }
           }
         }
       }
 
-      std::ostringstream message;
-      message << "Worklist C-Find: parsed " << parsedFilesCount
-              << " files, found " << matchedWorklistCount << " match(es)";
-      OrthancPlugins::LogInfo(message.str());
+      LOG(INFO) << "Worklist C-Find: parsed " << parsedFilesCount
+                << " files, found " << matchedWorklistCount << " match(es)";
     }
     catch (fs::filesystem_error&)
     {
-      OrthancPlugins::LogError("Inexistent folder while scanning for worklists: " + source.string());
+      LOG(ERROR) << "Inexistent folder while scanning for worklists: " << source.string();
       return OrthancPluginErrorCode_DirectoryExpected;
     }
 
@@ -216,7 +214,8 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* c)
   {
-    OrthancPlugins::SetGlobalContext(c);
+    Orthanc::Logging::InitializePluginContext(c, MODALITY_WORKLISTS_NAME);
+    OrthancPlugins::SetGlobalContext(c, MODALITY_WORKLISTS_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(c) == 0)
@@ -227,7 +226,7 @@
       return -1;
     }
 
-    OrthancPlugins::LogWarning("Sample worklist plugin is initializing");
+    LOG(WARNING) << "Sample worklist plugin is initializing";
     OrthancPluginSetDescription2(c, MODALITY_WORKLISTS_NAME, "Serve DICOM modality worklists from a folder with Orthanc.");
 
     OrthancPlugins::OrthancConfiguration configuration;
@@ -240,12 +239,12 @@
     {
       if (worklists.LookupStringValue(folder_, "Database"))
       {
-        OrthancPlugins::LogWarning("The database of worklists will be read from folder: " + folder_);
+        LOG(WARNING) << "The database of worklists will be read from folder: " << folder_;
         OrthancPluginRegisterWorklistCallback(OrthancPlugins::GetGlobalContext(), Callback);
       }
       else
       {
-        OrthancPlugins::LogError("The configuration option \"Worklists.Database\" must contain a path");
+        LOG(ERROR) << "The configuration option \"Worklists.Database\" must contain a path";
         return -1;
       }
 
@@ -254,7 +253,7 @@
     }
     else
     {
-      OrthancPlugins::LogWarning("Worklist server is disabled by the configuration file");
+      LOG(WARNING) << "Worklist server is disabled by the configuration file";
     }
 
     return 0;
@@ -263,7 +262,7 @@
 
   ORTHANC_PLUGINS_API void OrthancPluginFinalize()
   {
-    OrthancPlugins::LogWarning("Sample worklist plugin is finalizing");
+    LOG(WARNING) << "Sample worklist plugin is finalizing";
   }
 
 
--- a/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/MultitenantDicom/Plugin.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -120,7 +120,8 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    OrthancPlugins::SetGlobalContext(context);
+    OrthancPlugins::SetGlobalContext(context, ORTHANC_PLUGIN_NAME);
+    Orthanc::Logging::InitializePluginContext(context, ORTHANC_PLUGIN_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(OrthancPlugins::GetGlobalContext()) == 0)
@@ -135,18 +136,6 @@
       return -1;
     }
 
-#if ORTHANC_FRAMEWORK_VERSION_IS_ABOVE(1, 7, 2)
-    Orthanc::Logging::InitializePluginContext(context);
-#else
-    Orthanc::Logging::Initialize(context);
-#endif
-
-    if (!OrthancPlugins::CheckMinimalOrthancVersion(1, 12, 0))
-    {
-      OrthancPlugins::ReportMinimalOrthancVersion(1, 12, 0);
-      return -1;
-    }
-
     OrthancPluginSetDescription2(context, ORTHANC_PLUGIN_NAME, "Multitenant plugin for Orthanc.");
 
     OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
--- a/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Plugins/Samples/ServeFolders/Plugin.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -23,6 +23,7 @@
 #define SERVE_FOLDERS_NAME "serve-folders"
 
 #include "../Common/OrthancPluginCppWrapper.h"
+#include "../../../OrthancFramework/Sources/Logging.h"
 
 #include <json/value.h>
 #include <boost/filesystem.hpp>
@@ -94,7 +95,7 @@
   }
   else
   {
-    OrthancPlugins::LogWarning("ServeFolders: Unknown MIME type for extension \"" + extension + "\"");
+    LOG(WARNING) << "ServeFolders: Unknown MIME type for extension \"" << extension << "\"";
     return "application/octet-stream";
   }
 }
@@ -109,7 +110,7 @@
   std::map<std::string, std::string>::const_iterator found = folders_.find(uri);
   if (found == folders_.end())
   {
-    OrthancPlugins::LogError("Unknown URI in plugin server-folders: " + uri);
+    LOG(ERROR) << "Unknown URI in plugin server-folders: " << uri;
     OrthancPluginSendHttpStatusCode(OrthancPlugins::GetGlobalContext(), output, 404);
     return false;
   }
@@ -265,7 +266,7 @@
 {
   if (folders.type() != Json::objectValue)
   {
-    OrthancPlugins::LogError("The list of folders to be served is badly formatted (must be a JSON object)");
+    LOG(ERROR) << "The list of folders to be served is badly formatted (must be a JSON object)";
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
 
@@ -277,8 +278,8 @@
   {
     if (folders[*it].type() != Json::stringValue)
     {
-      OrthancPlugins::LogError("The folder to be served \"" + *it + 
-                               "\" must be associated with a string value (its mapped URI)");
+      LOG(ERROR) << "The folder to be served \"" << *it << 
+                    "\" must be associated with a string value (its mapped URI)";
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -299,7 +300,7 @@
 
     if (baseUri.empty())
     {
-      OrthancPlugins::LogError("The URI of a folder to be served cannot be empty");
+      LOG(ERROR) << "The URI of a folder to be served cannot be empty";
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -307,7 +308,7 @@
     const std::string folder = folders[*it].asString();
     if (!boost::filesystem::is_directory(folder))
     {
-      OrthancPlugins::LogError("Trying to serve an inexistent folder: " + folder);
+      LOG(ERROR) << "Trying to serve an inexistent folder: " + folder;
       ORTHANC_PLUGINS_THROW_EXCEPTION(InexistentFile);
     }
 
@@ -326,7 +327,7 @@
 {
   if (extensions.type() != Json::objectValue)
   {
-    OrthancPlugins::LogError("The list of extensions is badly formatted (must be a JSON object)");
+    LOG(ERROR) << "The list of extensions is badly formatted (must be a JSON object)";
     ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
   }
 
@@ -337,8 +338,8 @@
   {
     if (extensions[*it].type() != Json::stringValue)
     {
-      OrthancPlugins::LogError("The file extension \"" + *it + 
-                               "\" must be associated with a string value (its MIME type)");
+      LOG(ERROR) << "The file extension \"" << *it << 
+                    "\" must be associated with a string value (its MIME type)";
       ORTHANC_PLUGINS_THROW_EXCEPTION(BadFileFormat);
     }
 
@@ -356,13 +357,11 @@
 
     if (mime.empty())
     {
-      OrthancPlugins::LogWarning("ServeFolders: Removing MIME type for file extension \"." +
-                                 name + "\"");
+      LOG(WARNING) << "ServeFolders: Removing MIME type for file extension \"." << name << "\"";
     }
     else
     {
-      OrthancPlugins::LogWarning("ServeFolders: Associating file extension \"." + name + 
-                                 "\" with MIME type \"" + mime + "\"");
+      LOG(WARNING) << "ServeFolders: Associating file extension \"." << name << "\" with MIME type \"" << mime << "\"";
     }
   }  
 }
@@ -392,17 +391,13 @@
     if (configuration.LookupBooleanValue(tmp, "AllowCache"))
     {
       allowCache_ = tmp;
-      OrthancPlugins::LogWarning("ServeFolders: Requesting the HTTP client to " +
-                                 std::string(tmp ? "enable" : "disable") + 
-                                 " its caching mechanism");
+      LOG(WARNING) << "ServeFolders: Requesting the HTTP client to " << (tmp ? "enable" : "disable") << " its caching mechanism";
     }
 
     if (configuration.LookupBooleanValue(tmp, "GenerateETag"))
     {
       generateETag_ = tmp;
-      OrthancPlugins::LogWarning("ServeFolders: The computation of an ETag for the "
-                                 "served resources is " +
-                                 std::string(tmp ? "enabled" : "disabled"));
+      LOG(WARNING) << "ServeFolders: The computation of an ETag for the served resources is " << (tmp ? "enabled" : "disabled");
     }
 
     OrthancPlugins::OrthancConfiguration extensions;
@@ -412,8 +407,7 @@
 
   if (folders_.empty())
   {
-    OrthancPlugins::LogWarning("ServeFolders: Empty configuration file: "
-                               "No additional folder will be served!");
+    LOG(WARNING) << "ServeFolders: Empty configuration file: No additional folder will be served!";
   }
 }
 
@@ -422,7 +416,8 @@
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
   {
-    OrthancPlugins::SetGlobalContext(context);
+    OrthancPlugins::SetGlobalContext(context, SERVE_FOLDERS_NAME);
+    Orthanc::Logging::InitializePluginContext(context, SERVE_FOLDERS_NAME);
 
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(context) == 0)
@@ -444,8 +439,7 @@
     }
     catch (OrthancPlugins::PluginException& e)
     {
-      OrthancPlugins::LogError("Error while initializing the ServeFolders plugin: " + 
-                               std::string(e.What(context)));
+      LOG(ERROR) << "Error while initializing the ServeFolders plugin: " << e.What(context);
     }
 
     return 0;
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -2796,13 +2796,15 @@
   }
 
 
-  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom)
+  void StatelessDatabaseOperations::ReconstructInstance(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
   {
     class Operations : public IReadWriteOperations
     {
     private:
       DicomMap                              summary_;
       std::unique_ptr<DicomInstanceHasher>  hasher_;
+      bool                                  limitToThisLevelDicomTags_;
+      ResourceType                          limitToLevel_;
       bool                                  hasTransferSyntax_;
       DicomTransferSyntax                   transferSyntax_;
 
@@ -2844,7 +2846,9 @@
       }
 
     public:
-      explicit Operations(const ParsedDicomFile& dicom)
+      explicit Operations(const ParsedDicomFile& dicom, bool limitToThisLevelDicomTags, ResourceType limitToLevel)
+      : limitToThisLevelDicomTags_(limitToThisLevelDicomTags),
+        limitToLevel_(limitToLevel)
       {
         OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
         hasher_.reset(new DicomInstanceHasher(summary_));
@@ -2872,48 +2876,76 @@
           throw OrthancException(ErrorCode_InternalError);
         }
 
-        transaction.ClearMainDicomTags(patient);
-        transaction.ClearMainDicomTags(study);
-        transaction.ClearMainDicomTags(series);
-        transaction.ClearMainDicomTags(instance);
-
+        if (limitToThisLevelDicomTags_)
         {
           ResourcesContent content(false /* prevent the setting of metadata */);
-          content.AddResource(patient, ResourceType_Patient, summary_);
-          content.AddResource(study, ResourceType_Study, summary_);
-          content.AddResource(series, ResourceType_Series, summary_);
-          content.AddResource(instance, ResourceType_Instance, summary_);
-
+          int64_t resource = -1;
+          if (limitToLevel_ == ResourceType_Patient)
+          {
+            resource = patient;
+          }
+          else if (limitToLevel_ == ResourceType_Study)
+          {
+            resource = study;
+          }
+          else if (limitToLevel_ == ResourceType_Series)
+          {
+            resource = series;
+          }
+          else if (limitToLevel_ == ResourceType_Instance)
+          {
+            resource = instance;
+          }
+
+          transaction.ClearMainDicomTags(resource);
+          content.AddResource(resource, limitToLevel_, summary_);
           transaction.SetResourcesContent(content);
-
-          ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
-          ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
-        
-          SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient);
-          SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study);
-          SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series);
-          SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance);
+          ReplaceMetadata(transaction, resource, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(limitToLevel_));
         }
-
-        if (hasTransferSyntax_)
+        else
         {
-          ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+          transaction.ClearMainDicomTags(patient);
+          transaction.ClearMainDicomTags(study);
+          transaction.ClearMainDicomTags(series);
+          transaction.ClearMainDicomTags(instance);
+
+          {
+            ResourcesContent content(false /* prevent the setting of metadata */);
+            content.AddResource(patient, ResourceType_Patient, summary_);
+            content.AddResource(study, ResourceType_Study, summary_);
+            content.AddResource(series, ResourceType_Series, summary_);
+            content.AddResource(instance, ResourceType_Instance, summary_);
+
+            transaction.SetResourcesContent(content);
+
+            ReplaceMetadata(transaction, patient, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Patient));    // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, study, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Study));        // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, series, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Series));      // New in Orthanc 1.11.0
+            ReplaceMetadata(transaction, instance, MetadataType_MainDicomTagsSignature, DicomMap::GetMainDicomTagsSignature(ResourceType_Instance));  // New in Orthanc 1.11.0
+          
+            SetMainDicomSequenceMetadata(transaction, patient, summary_, ResourceType_Patient);
+            SetMainDicomSequenceMetadata(transaction, study, summary_, ResourceType_Study);
+            SetMainDicomSequenceMetadata(transaction, series, summary_, ResourceType_Series);
+            SetMainDicomSequenceMetadata(transaction, instance, summary_, ResourceType_Instance);
+          }
+
+          if (hasTransferSyntax_)
+          {
+            ReplaceMetadata(transaction, instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
+          }
+
+          const DicomValue* value;
+          if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+              !value->IsNull() &&
+              !value->IsBinary())
+          {
+            ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
+          }
         }
-
-        const DicomValue* value;
-        if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          ReplaceMetadata(transaction, instance, MetadataType_Instance_SopClassUid, value->GetContent());
-        }
-
       }
     };
 
-    Operations operations(dicom);
+    Operations operations(dicom, limitToThisLevelDicomTags, limitToLevel);
     Apply(operations);
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Thu Apr 25 17:07:33 2024 +0200
@@ -778,7 +778,9 @@
                    const std::string& publicId,
                    ResourceType level);
 
-    void ReconstructInstance(const ParsedDicomFile& dicom);
+    void ReconstructInstance(const ParsedDicomFile& dicom, 
+                             bool limitToThisLevelDicomTags, 
+                             ResourceType limitToLevel_);
 
     StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
                       const DicomMap& dicomSummary,
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -621,7 +621,7 @@
   {
       call.GetDocumentation().SetHttpGetArgument(GET_REQUESTED_TAGS, RestApiCallDocumentation::Type_String,
                           "If present, list the DICOM Tags you want to list in the response.  This argument is a semi-column separated list "
-                          "of DICOM Tags identifiers; e.g: 'requestedTags=0010,0010;PatientBirthDate'.  "
+                          "of DICOM Tags identifiers; e.g: '" + GET_REQUESTED_TAGS + "=0010,0010;PatientBirthDate'.  "
                           "The tags requested tags are returned in the 'RequestedTags' field in the response.  "
                           "Note that, if you are requesting tags that are not listed in the Main Dicom Tags stored in DB, building the response "
                           "might be slow since Orthanc will need to access the DICOM files.  If not specified, Orthanc will return ", false);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestResources.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -58,6 +58,7 @@
 
 static const char* const IGNORE_LENGTH = "ignore-length";
 static const char* const RECONSTRUCT_FILES = "ReconstructFiles";
+static const char* const LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS = "LimitToThisLevelMainDicomTags";
 
 
 namespace Orthanc
@@ -3734,12 +3735,21 @@
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
-  void DocumentReconstructFilesField(RestApiPostCall& call)
+  void DocumentReconstructFilesField(RestApiPostCall& call, bool documentLimitField)
   {
     call.GetDocumentation()
       .SetRequestField(RECONSTRUCT_FILES, RestApiCallDocumentation::Type_Boolean,
                        "Also reconstruct the files of the resources (e.g: apply IngestTranscoding, StorageCompression). "
                        "'false' by default. (New in Orthanc 1.11.0)", false);
+    if (documentLimitField)
+    {
+      call.GetDocumentation()
+        .SetRequestField(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS, RestApiCallDocumentation::Type_Boolean,
+                        "Only reconstruct this level MainDicomTags by re-reading them from a random child instance of the resource. "
+                        "This option is much faster than a full reconstruct and is usefull e.g. if you have modified the "
+                        "'ExtraMainDicomTags' at the Study level to optimize the speed of some C-Find. "
+                        "'false' by default. (New in Orthanc 1.12.4)", false);
+    }
   }
 
   bool GetReconstructFilesField(const RestApiPostCall& call)
@@ -3761,6 +3771,26 @@
     return reconstructFiles;
   }
 
+  bool GetLimitToThisLevelMainDicomTags(const RestApiPostCall& call)
+  {
+    bool limitToThisLevel = false;
+    Json::Value request;
+
+    if (call.GetBodySize() > 0 && call.ParseJsonRequest(request) && request.isMember(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS))
+    {
+      if (!request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].isBool())
+      {
+        throw OrthancException(ErrorCode_BadFileFormat,
+                               "The field " + std::string(LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS) + " must contain a Boolean");
+      }
+
+      limitToThisLevel = request[LIMIT_TO_THIS_LEVEL_MAIN_DICOM_TAGS].asBool();
+    }
+
+    return limitToThisLevel;
+  }
+
+
   template <enum ResourceType type>
   static void ReconstructResource(RestApiPostCall& call)
   {
@@ -3776,13 +3806,13 @@
                         "Beware that this is a time-consuming operation, as all the children DICOM instances will be "
                         "parsed again, and the Orthanc index will be updated accordingly.")
         .SetUriArgument("id", "Orthanc identifier of the " + resource + " of interest");
-        DocumentReconstructFilesField(call);
+        DocumentReconstructFilesField(call, true);
 
       return;
     }
 
     ServerContext& context = OrthancRestApi::GetContext(call);
-    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call));
+    ServerToolbox::ReconstructResource(context, call.GetUriComponent("id", ""), GetReconstructFilesField(call), GetLimitToThisLevelMainDicomTags(call), type);
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
   }
 
@@ -3800,7 +3830,7 @@
                         "as all the DICOM instances will be parsed again, and as all the Orthanc index will be regenerated. "
                         "If you have a large database to process, it is advised to use the Housekeeper plugin to perform "
                         "this action resource by resource");
-        DocumentReconstructFilesField(call);
+        DocumentReconstructFilesField(call, false);
 
       return;
     }
@@ -3814,7 +3844,7 @@
     for (std::list<std::string>::const_iterator 
            study = studies.begin(); study != studies.end(); ++study)
     {
-      ServerToolbox::ReconstructResource(context, *study, reconstructFiles);
+      ServerToolbox::ReconstructResource(context, *study, reconstructFiles, false, ResourceType_Study /*  dummy */);
     }
     
     call.GetOutput().AnswerBuffer("", MimeType_PlainText);
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -186,7 +186,7 @@
         ServerContext::DicomCacheLocker locker(GetContext(), *it);
         ParsedDicomFile& modifiedDicom = locker.GetDicom();
 
-        GetContext().GetIndex().ReconstructInstance(modifiedDicom);
+        GetContext().GetIndex().ReconstructInstance(modifiedDicom, false, ResourceType_Instance /* dummy */);
       }
     }
     
--- a/OrthancServer/Sources/ServerToolbox.cpp	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/ServerToolbox.cpp	Thu Apr 25 17:07:33 2024 +0200
@@ -280,32 +280,43 @@
     
     void ReconstructResource(ServerContext& context,
                              const std::string& resource,
-                             bool reconstructFiles)
+                             bool reconstructFiles,
+                             bool limitToThisLevelDicomTags,
+                             ResourceType limitToLevel)
     {
       LOG(WARNING) << "Reconstructing resource " << resource;
       
       std::list<std::string> instances;
       context.GetIndex().GetChildInstances(instances, resource);
 
-      for (std::list<std::string>::const_iterator 
-             it = instances.begin(); it != instances.end(); ++it)
+
+      if (limitToThisLevelDicomTags && instances.size() > 0) // in this case, we only need to rebuild one instance !
       {
-        ServerContext::DicomCacheLocker locker(context, *it);
+        ServerContext::DicomCacheLocker locker(context, instances.front());
+        context.GetIndex().ReconstructInstance(locker.GetDicom(), true, limitToLevel);
+      }
+      else
+      {
+        for (std::list<std::string>::const_iterator 
+              it = instances.begin(); it != instances.end(); ++it)
+        {
+          ServerContext::DicomCacheLocker locker(context, *it);
 
-        // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
-        context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
-                                            -1 /* dummy revision */, "" /* dummy MD5 */);
-        
-        context.GetIndex().ReconstructInstance(locker.GetDicom());
+          // Delay the reconstruction of DICOM-as-JSON to its next access through "ServerContext"
+          context.GetIndex().DeleteAttachment(*it, FileContentType_DicomAsJson, false /* no revision */,
+                                              -1 /* dummy revision */, "" /* dummy MD5 */);
+          
+          context.GetIndex().ReconstructInstance(locker.GetDicom(), false, ResourceType_Instance /* dummy */);
 
-        if (reconstructFiles)
-        {
-          std::string resultPublicId;  // ignored
-          std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
+          if (reconstructFiles)
+          {
+            std::string resultPublicId;  // ignored
+            std::unique_ptr<DicomInstanceToStore> dicomInstancetoStore(DicomInstanceToStore::CreateFromParsedDicomFile(locker.GetDicom()));
 
-          // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter
-          // we should very likely refactor it
-          context.TranscodeAndStore(resultPublicId, dicomInstancetoStore.get(), StoreInstanceMode_OverwriteDuplicate, true);
+            // TODO: TranscodeAndStore and specifically ServerIndex::Store have been "poluted" by the isReconstruct parameter
+            // we should very likely refactor it
+            context.TranscodeAndStore(resultPublicId, dicomInstancetoStore.get(), StoreInstanceMode_OverwriteDuplicate, true);
+          }
         }
       }
     }
--- a/OrthancServer/Sources/ServerToolbox.h	Thu Apr 25 09:22:07 2024 +0200
+++ b/OrthancServer/Sources/ServerToolbox.h	Thu Apr 25 17:07:33 2024 +0200
@@ -55,7 +55,9 @@
 
     void ReconstructResource(ServerContext& context,
                              const std::string& resource,
-                             bool reconstructFiles);
+                             bool reconstructFiles,
+                             bool limitToThisLevelDicomTags,
+                             ResourceType limitToLevel);
 
     bool IsValidLabel(const std::string& label);
 
--- a/TODO	Thu Apr 25 09:22:07 2024 +0200
+++ b/TODO	Thu Apr 25 17:07:33 2024 +0200
@@ -56,6 +56,13 @@
   - ...
 * Investigate if one could fix KeepAlive race conditions:
   https://discourse.orthanc-server.org/t/socket-hangup-with-rest-api/4023/3
+* The DICOM file cache shall keep a MD5 of the cached file and compare it with MD5
+  from the DB.  That would allow 2 orthancs in a swarm to realize when the other
+  Orthanc has updated the file:
+  https://discourse.orthanc-server.org/t/instances-id-content-api-results-are-different-in-docker-swarm-replicas-of-orthanc/4582
+* Allow saving PrivateTags in ExtraMainDicomTags.
+  Note: they can actually be stored but they then appear as "Unknown Tag & Data" in the responses.
+  If we try to add the PrivateCreator in the ExtraMainDicomTags, then, the DICOMWeb plugin fails to initialize because the private tags are not known.
 
 ============================
 Documentation (Orthanc Book)
@@ -258,8 +265,6 @@
 * Provide a C++ callback similar to "ReceivedInstanceFilter()" in Lua
   https://orthanc.uclouvain.be/book/users/lua.html#filtering-incoming-dicom-instances
   https://groups.google.com/d/msg/orthanc-users/BtvLTE5Ni8A/vIMhmMgfBAAJ
-* In "OrthancPluginLog[Error|Warning|Info]()", prefix the log line with
-  the name of the plugin, as retrieved by "OrthancPluginGetName()"
 * Update the SDK to handle buffer sizes > 4GB (all sizes are currently coded in uint32_t)
 * Add a C-Get SCP handler: OrthancPluginRegisterGetCallback
   https://groups.google.com/g/orthanc-users/c/NRhPkYX9IXQ/m/mWS11g0jBwAJ