changeset 745:b4f6c00c343f

added 2 metrics for WADO-RS
author Alain Mazy <am@orthanc.team>
date Fri, 13 Mar 2026 18:02:12 +0100
parents fce05c8b00fb
children a760a8b2c64c
files NEWS Plugin/Plugin.cpp Plugin/WadoRs.cpp Plugin/WadoRs.h Plugin/WeightedAverageMetrics.h Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
diffstat 7 files changed, 201 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Fri Feb 13 14:18:28 2026 +0100
+++ b/NEWS	Fri Mar 13 18:02:12 2026 +0100
@@ -3,6 +3,14 @@
 
 * If calling /rendered route on a Encapsulated PDF, the plugin will now return the pdf file
   instead of generating a 400 error.
+* Added metrics:
+  - orthanc_dicomweb_wadors_average_bandwidth_per_call_mbytes_per_second_5m is the weighted
+    average bandwidth of each individual call to any WADO-RS route to retrieve instances, series
+    or studies.  This metrics is only updated at the end of the call and averages the bandwidth
+    values that are also displayed in the logs if you set "EnablePerformanceLogs" to true.
+  - orthanc_dicomweb_wadors_total_bytes_transferred counts the number of bytes that have
+    been returned by any WADO-RS route to retrieve instances, series or studies.  This is updated
+    while the response is returned.
 
 
 Version 1.22 (2025-12-02)
--- a/Plugin/Plugin.cpp	Fri Feb 13 14:18:28 2026 +0100
+++ b/Plugin/Plugin.cpp	Fri Mar 13 18:02:12 2026 +0100
@@ -466,6 +466,11 @@
 #endif
 
 
+static void RefreshMetricsCallback()
+{
+  RefreshWadoRsMetrics();
+}
+
 static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, 
                                                OrthancPluginResourceType resourceType, 
                                                const char *resourceId)
@@ -711,6 +716,7 @@
 
         OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
 
+        OrthancPluginRegisterRefreshMetricsCallback(context, RefreshMetricsCallback);
 
         // Extend the default Orthanc Explorer with custom JavaScript for STOW client
         std::string explorer;
--- a/Plugin/WadoRs.cpp	Fri Feb 13 14:18:28 2026 +0100
+++ b/Plugin/WadoRs.cpp	Fri Mar 13 18:02:12 2026 +0100
@@ -43,6 +43,7 @@
 #include <boost/thread.hpp>
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string/predicate.hpp>
+#include "WeightedAverageMetrics.h"
 
 static const std::string SERIES_METADATA_ATTACHMENT_ID = "4301";
 static std::string WADO_BASE_PLACEHOLDER = "$WADO_BASE_PLACEHOLDER$";
@@ -59,7 +60,10 @@
 
 static boost::mutex preloaderThreadsCounterMutex;
 static uint32_t preloaderThreadsCounter = 0;
+static WeightedAverageMetrics<float> wadorsAverageBandwidth(300);
 
+static boost::mutex wadoRsTotalBytesTransferredMutex;
+static int64_t wadoRsTotalBytesTransferred = 0;
 
 void SetPluginCanUseExtendedFind(bool enable)
 {
@@ -81,6 +85,12 @@
   return isSystemReadOnly_;
 }
 
+void RefreshWadoRsMetrics()
+{
+  OrthancPlugins::SetMetricsValue("orthanc_dicomweb_wadors_average_bandwidth_per_call_mbytes_per_second_5m", wadorsAverageBandwidth.GetAverage());
+  OrthancPlugins::SetMetricsValue("orthanc_dicomweb_wadors_total_bytes_transferred", wadoRsTotalBytesTransferred);
+}
+
 static std::string GetResourceUri(Orthanc::ResourceType level,
                                   const std::string& publicId)
 {
@@ -768,6 +778,9 @@
         throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
       }
       perfTotalSizeInBytes += dicom->GetSize();
+
+      boost::mutex::scoped_lock lock(wadoRsTotalBytesTransferredMutex);
+      wadoRsTotalBytesTransferred += static_cast<int64_t>(dicom->GetSize());
     }
     else
     {
@@ -775,9 +788,13 @@
     }
   }
 
+  uint64_t elapsedMicrosends = perfTimer.GetElapsedMicroseconds();
+
+  float bandwidth = float(perfTotalSizeInBytes) / float(elapsedMicrosends) * 8.0f; // this gives a bandwidth in MBps
+  wadorsAverageBandwidth.AddValue(bandwidth, float(perfTotalSizeInBytes));
+
   if (OrthancPlugins::Configuration::IsPerformanceLogsEnabled())
   {
-    uint64_t elapsedMicrosends = perfTimer.GetElapsedMicroseconds();
     float instancesPerSeconds = float(perfTotalInstancesCount) / (float(elapsedMicrosends) / 1000000.0f);
     LOG(INFO) << "WADO-RS: elapsed: " << perfTimer.GetElapsedMicroseconds() << " us, rate: " << std::fixed << std::setprecision(2) << instancesPerSeconds << " instances/s, " << Orthanc::Toolbox::GetHumanTransferSpeed(false, perfTotalSizeInBytes, elapsedMicrosends * 1000);
   }
--- a/Plugin/WadoRs.h	Fri Feb 13 14:18:28 2026 +0100
+++ b/Plugin/WadoRs.h	Fri Mar 13 18:02:12 2026 +0100
@@ -118,4 +118,6 @@
 
 void SetPluginCanUseExtendedFind(bool enable);
 
-void SetSystemIsReadOnly(bool isReadOnly);
\ No newline at end of file
+void SetSystemIsReadOnly(bool isReadOnly);
+
+void RefreshWadoRsMetrics();
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/WeightedAverageMetrics.h	Fri Mar 13 18:02:12 2026 +0100
@@ -0,0 +1,118 @@
+/**
+ * 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-2026 Orthanc Team SRL, Belgium
+ * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <boost/thread/mutex.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <deque>
+#include <stdint.h>
+
+
+template <typename T>
+class WeightedAverageMetrics : public boost::noncopyable
+{
+  struct Value
+  {
+    boost::posix_time::ptime time_;
+    T                        value_;
+    T                        weight_;
+
+    Value(const T& value, const T& weight) :
+    time_(boost::posix_time::microsec_clock::universal_time()),
+    value_(value),
+    weight_(weight)
+    {
+    }
+  };
+
+private:
+  std::deque<Value>          values_;
+  T                          totalWeightedValue_;
+  T                          totalWeight_;
+  int64_t                    duration_;
+  boost::mutex               mutex_;
+
+  void RemoveOldest()
+  {
+    boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+
+    if (values_.size() > 0)
+    {
+      Value& oldest = values_.front();
+      while ((now - oldest.time_).total_seconds() > duration_)
+      {
+        totalWeightedValue_ -= oldest.value_ * oldest.weight_;
+        totalWeight_ -= oldest.weight_;
+        values_.pop_front();
+        if (values_.size() > 0)
+        {
+          oldest = values_.front();
+        }
+      }
+    }
+  }
+
+public:
+  WeightedAverageMetrics(int64_t duration) :
+    totalWeightedValue_(0),
+    totalWeight_(0),
+    duration_(duration)
+  {
+  }
+
+  void AddValue(const T& value, const T& weight)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    values_.push_back(Value(value, weight));
+    totalWeightedValue_ += value * weight;
+    totalWeight_ += weight;
+    boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+
+    const Value& oldest = values_.front();
+    while ((now - oldest.time_).total_seconds() > duration_)
+    {
+      totalWeightedValue_ -= oldest.value_ * oldest.weight_;
+      totalWeight_ -= oldest.weight_;
+      values_.pop_front();
+    }
+  }
+
+  T GetAverage()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    RemoveOldest();
+
+    if (totalWeight_ > 0)
+    {
+      return totalWeightedValue_ / totalWeight_;
+    }
+    else
+    {
+      return 0;
+    }
+  }
+};
+
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Fri Feb 13 14:18:28 2026 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Fri Mar 13 18:02:12 2026 +0100
@@ -1544,11 +1544,25 @@
 
 #endif /* HAS_ORTHANC_PLUGIN_FIND_MATCHER == 1 */
 
+  static void CheckAnswerSizeIsLessThan4GB(const std::string& answer)
+  {
+    if (answer.size() > static_cast<size_t>(std::numeric_limits<uint32_t>::max()))
+    {
+  #if HAS_ORTHANC_EXCEPTION == 1
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange, "Cannot send HTTP response larger than 4GB");
+  #else
+      ORTHANC_PLUGINS_LOG_ERROR("Cannot send HTTP response larger than 4GB");
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_ParameterOutOfRange);          
+  #endif
+    }
+  }
+
   void AnswerJson(const Json::Value& value,
                   OrthancPluginRestOutput* output)
   {
     std::string bodyString;
-    WriteStyledJson(bodyString, value);    
+    WriteStyledJson(bodyString, value);
+    CheckAnswerSizeIsLessThan4GB(bodyString);    
     OrthancPluginAnswerBuffer(GetGlobalContext(), output, bodyString.c_str(), bodyString.size(), "application/json");
   }
 
@@ -1556,6 +1570,7 @@
                     const char* mimeType,
                     OrthancPluginRestOutput* output)
   {
+    CheckAnswerSizeIsLessThan4GB(answer);
     OrthancPluginAnswerBuffer(GetGlobalContext(), output, answer.c_str(), answer.size(), mimeType);
   }
 
@@ -1564,6 +1579,26 @@
     OrthancPluginSendHttpStatusCode(GetGlobalContext(), output, httpError);
   }
 
+  void AnswerHttpError(uint16_t httpError,
+                       OrthancPluginRestOutput* output,
+                       const std::string& answer,
+                       const char* mimeType)
+  {
+    CheckAnswerSizeIsLessThan4GB(answer);
+
+    OrthancPluginSetHttpHeader(GetGlobalContext(),
+                               output,
+                               "content-type",
+                               mimeType);
+                               
+    OrthancPluginSendHttpStatus(GetGlobalContext(),
+                                output,
+                                httpError,
+                                answer.c_str(),
+                                static_cast<uint32_t>(answer.size()));
+  }
+
+
   void AnswerMethodNotAllowed(OrthancPluginRestOutput *output, const char* allowedMethods)
   {
     OrthancPluginSendMethodNotAllowed(GetGlobalContext(), output, allowedMethods);
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Fri Feb 13 14:18:28 2026 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Fri Mar 13 18:02:12 2026 +0100
@@ -690,6 +690,11 @@
   void AnswerHttpError(uint16_t httpError,
                        OrthancPluginRestOutput* output);
 
+  void AnswerHttpError(uint16_t httpError,
+                       OrthancPluginRestOutput* output,
+                       const std::string& answer,
+                       const char* mimeType);
+
   void AnswerMethodNotAllowed(OrthancPluginRestOutput* output, const char* allowedMethods);
 
 #if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 0)
@@ -1026,6 +1031,13 @@
                                  value, OrthancPluginMetricsType_Default);
   }
 
+  inline void SetMetricsValue(const char* name,
+                              int64_t value)
+  {
+    OrthancPluginSetMetricsIntegerValue(GetGlobalContext(), name,
+                                        value, OrthancPluginMetricsType_Default);
+  }
+
   class MetricsTimer : public boost::noncopyable
   {
   private: