# HG changeset patch # User Sebastien Jodogne # Date 1548771348 -3600 # Node ID 8ea7c4546c3a9712fd7727d24c2782e15aedc340 # Parent 096f4a29f2239a526ef9b6a0f3dc5f4b604a019e primitives to collect metrics in Orthanc diff -r 096f4a29f223 -r 8ea7c4546c3a Core/Enumerations.cpp --- a/Core/Enumerations.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/Core/Enumerations.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -1103,6 +1103,10 @@ case MimeType_Woff: return MIME_WOFF; + + case MimeType_PrometheusText: + // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format + return "text/plain; version=0.0.4"; default: throw OrthancException(ErrorCode_ParameterOutOfRange); diff -r 096f4a29f223 -r 8ea7c4546c3a Core/Enumerations.h --- a/Core/Enumerations.h Tue Jan 29 10:34:00 2019 +0100 +++ b/Core/Enumerations.h Tue Jan 29 15:15:48 2019 +0100 @@ -104,8 +104,9 @@ MimeType_Svg, MimeType_WebAssembly, MimeType_Xml, - MimeType_Woff, // Web Open Font Format - MimeType_Zip + MimeType_Woff, // Web Open Font Format + MimeType_Zip, + MimeType_PrometheusText // Prometheus text-based exposition format (for metrics) }; diff -r 096f4a29f223 -r 8ea7c4546c3a Core/MetricsRegistry.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MetricsRegistry.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -0,0 +1,321 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#include "PrecompiledHeaders.h" +#include "MetricsRegistry.h" + +#include "OrthancException.h" +#include "ChunkedBuffer.h" + +namespace Orthanc +{ + static const boost::posix_time::ptime GetNow() + { + return boost::posix_time::second_clock::universal_time(); + } + + + class MetricsRegistry::Item + { + private: + MetricsType type_; + boost::posix_time::ptime time_; + bool hasValue_; + float value_; + + void Touch(float value, + const boost::posix_time::ptime& now) + { + hasValue_ = true; + value_ = value; + time_ = now; + } + + void Touch(float value) + { + Touch(value, GetNow()); + } + + void UpdateMax(float value, + int duration) + { + if (hasValue_) + { + const boost::posix_time::ptime now = GetNow(); + + if (value > value_ || + (now - time_).total_seconds() > duration) + { + Touch(value, now); + } + } + else + { + Touch(value); + } + } + + void UpdateMin(float value, + int duration) + { + if (hasValue_) + { + const boost::posix_time::ptime now = GetNow(); + + if (value < value_ || + (now - time_).total_seconds() > duration) + { + Touch(value, now); + } + } + else + { + Touch(value); + } + } + + public: + Item(MetricsType type) : + type_(type), + hasValue_(false) + { + } + + MetricsType GetType() const + { + return type_; + } + + void Update(float value) + { + switch (type_) + { + case MetricsType_Default: + Touch(value); + break; + + case MetricsType_MaxOver10Seconds: + UpdateMax(value, 10); + break; + + case MetricsType_MaxOver1Minute: + UpdateMax(value, 60); + break; + + case MetricsType_MinOver10Seconds: + UpdateMin(value, 10); + break; + + case MetricsType_MinOver1Minute: + UpdateMin(value, 60); + break; + + default: + throw OrthancException(ErrorCode_NotImplemented); + } + } + + bool HasValue() const + { + return hasValue_; + } + + const boost::posix_time::ptime& GetTime() const + { + if (hasValue_) + { + return time_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + + float GetValue() const + { + if (hasValue_) + { + return value_; + } + else + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + } + }; + + + MetricsRegistry::~MetricsRegistry() + { + for (Content::iterator it = content_.begin(); it != content_.end(); ++it) + { + assert(it->second != NULL); + delete it->second; + } + } + + + void MetricsRegistry::SetEnabled(bool enabled) + { + boost::mutex::scoped_lock lock(mutex_); + enabled_ = enabled; + } + + + void MetricsRegistry::Register(const std::string& name, + MetricsType type) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::iterator found = content_.find(name); + + if (found == content_.end()) + { + content_[name] = new Item(type); + } + else + { + assert(found->second != NULL); + + // This metrics already exists: Only recreate it if there is a + // mismatch in the type of metrics + if (found->second->GetType() != type) + { + delete found->second; + found->second = new Item(type); + } + } + } + + + void MetricsRegistry::SetValueInternal(const std::string& name, + float value, + MetricsType type) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::iterator found = content_.find(name); + + if (found == content_.end()) + { + std::auto_ptr item(new Item(type)); + item->Update(value); + content_[name] = item.release(); + } + else + { + assert(found->second != NULL); + found->second->Update(value); + } + } + + + MetricsType MetricsRegistry::GetMetricsType(const std::string& name) + { + boost::mutex::scoped_lock lock(mutex_); + + Content::const_iterator found = content_.find(name); + + if (found == content_.end()) + { + throw OrthancException(ErrorCode_InexistentItem); + } + else + { + assert(found->second != NULL); + return found->second->GetType(); + } + } + + + void MetricsRegistry::ExportPrometheusText(std::string& s) + { + // https://www.boost.org/doc/libs/1_69_0/doc/html/date_time/examples.html#date_time.examples.seconds_since_epoch + static const boost::posix_time::ptime EPOCH(boost::gregorian::date(1970, 1, 1)); + + boost::mutex::scoped_lock lock(mutex_); + + s.clear(); + + if (!enabled_) + { + return; + } + + ChunkedBuffer buffer; + + for (Content::const_iterator it = content_.begin(); + it != content_.end(); ++it) + { + assert(it->second != NULL); + + if (it->second->HasValue()) + { + boost::posix_time::time_duration diff = it->second->GetTime() - EPOCH; + + std::string line = (it->first + " " + + boost::lexical_cast(it->second->GetValue()) + " " + + boost::lexical_cast(diff.total_milliseconds()) + "\n"); + + buffer.AddChunk(line); + } + } + + buffer.Flatten(s); + } + + + void MetricsRegistry::Timer::Start() + { + if (registry_.IsEnabled()) + { + active_ = true; + start_ = GetNow(); + } + else + { + active_ = false; + } + } + + + MetricsRegistry::Timer::~Timer() + { + if (active_) + { + boost::posix_time::time_duration diff = GetNow() - start_; + registry_.SetValue(name_, diff.total_milliseconds(), type_); + } + } +} diff -r 096f4a29f223 -r 8ea7c4546c3a Core/MetricsRegistry.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Core/MetricsRegistry.h Tue Jan 29 15:15:48 2019 +0100 @@ -0,0 +1,148 @@ +/** + * Orthanc - A Lightweight, RESTful DICOM Store + * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics + * Department, University Hospital of Liege, Belgium + * Copyright (C) 2017-2019 Osimis S.A., Belgium + * + * This program is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * In addition, as a special exception, the copyright holders of this + * program give permission to link the code of its release with the + * OpenSSL project's "OpenSSL" library (or with modified versions of it + * that use the same license as the "OpenSSL" library), and distribute + * the linked executables. You must obey the GNU General Public License + * in all respects for all of the code used other than "OpenSSL". If you + * modify file(s) with this exception, you may extend this exception to + * your version of the file(s), but you are not obligated to do so. If + * you do not wish to do so, delete this exception statement from your + * version. If you delete this exception statement from all source files + * in the program, then also delete it here. + * + * 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + **/ + + +#pragma once + +#if !defined(ORTHANC_SANDBOXED) +# error The macro ORTHANC_SANDBOXED must be defined +#endif + +#if ORTHANC_SANDBOXED == 1 +# error The class MetricsRegistry cannot be used in sandboxed environments +#endif + +#include +#include + +namespace Orthanc +{ + enum MetricsType + { + MetricsType_Default, + MetricsType_MaxOver10Seconds, + MetricsType_MaxOver1Minute, + MetricsType_MinOver10Seconds, + MetricsType_MinOver1Minute + }; + + class MetricsRegistry : public boost::noncopyable + { + private: + class Item; + + typedef std::map Content; + + bool enabled_; + boost::mutex mutex_; + Content content_; + + void SetValueInternal(const std::string& name, + float value, + MetricsType type); + + public: + MetricsRegistry() : + enabled_(true) + { + } + + ~MetricsRegistry(); + + bool IsEnabled() const + { + return enabled_; + } + + void SetEnabled(bool enabled); + + void Register(const std::string& name, + MetricsType type); + + void SetValue(const std::string& name, + float value, + MetricsType type) + { + // Inlining to avoid loosing time if metrics are disabled + if (enabled_) + { + SetValueInternal(name, value, type); + } + } + + void SetValue(const std::string& name, + float value) + { + SetValue(name, value, MetricsType_Default); + } + + MetricsType GetMetricsType(const std::string& name); + + // https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format + void ExportPrometheusText(std::string& s); + + + class Timer : public boost::noncopyable + { + private: + MetricsRegistry& registry_; + std::string name_; + MetricsType type_; + bool active_; + boost::posix_time::ptime start_; + + void Start(); + + public: + Timer(MetricsRegistry& registry, + const std::string& name) : + registry_(registry), + name_(name), + type_(MetricsType_MaxOver10Seconds) + { + Start(); + } + + Timer(MetricsRegistry& registry, + const std::string& name, + MetricsType type) : + registry_(registry), + name_(name), + type_(type) + { + Start(); + } + + ~Timer(); + }; + }; +} diff -r 096f4a29f223 -r 8ea7c4546c3a NEWS --- a/NEWS Tue Jan 29 10:34:00 2019 +0100 +++ b/NEWS Tue Jan 29 15:15:48 2019 +0100 @@ -1,6 +1,18 @@ Pending changes in the mainline =============================== +General +------- + +* New configuration option: "MetricsEnabled" to track the metrics of Orthanc + +REST API +-------- + +* API version has been upgraded to 1.4 +* New URI "/tools/metrics" to dynamically enable/disable the collection of metrics +* New URI "/tools/metrics-prometheus" to retrieve metrics using Prometheus text format + Version 1.5.3 (2019-01-25) ========================== diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/OrthancFindRequestHandler.cpp --- a/OrthancServer/OrthancFindRequestHandler.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/OrthancFindRequestHandler.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -38,6 +38,7 @@ #include "../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/Logging.h" #include "../Core/Lua/LuaFunctionCall.h" +#include "../Core/MetricsRegistry.h" #include "OrthancConfiguration.h" #include "Search/DatabaseLookup.h" #include "ServerContext.h" @@ -551,6 +552,8 @@ const std::string& calledAet, ModalityManufacturer manufacturer) { + MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_find_scp_duration_ms"); + /** * Possibly apply the user-supplied Lua filter. **/ diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/OrthancMoveRequestHandler.cpp --- a/OrthancServer/OrthancMoveRequestHandler.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/OrthancMoveRequestHandler.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -37,6 +37,7 @@ #include "../../Core/DicomParsing/FromDcmtkBridge.h" #include "../Core/DicomFormat/DicomArray.h" #include "../Core/Logging.h" +#include "../Core/MetricsRegistry.h" #include "OrthancConfiguration.h" #include "ServerContext.h" #include "ServerJobs/DicomModalityStoreJob.h" @@ -280,6 +281,8 @@ const std::string& calledAet, uint16_t originatorId) { + MetricsRegistry::Timer timer(context_.GetMetricsRegistry(), "orthanc_move_scp_duration_ms"); + LOG(WARNING) << "Move-SCU request received for AET \"" << targetAet << "\""; { diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/OrthancRestApi/OrthancRestResources.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -645,12 +645,47 @@ } - static void GetResourceStatistics(RestApiGetCall& call) { + static const uint64_t MEGA_BYTES = 1024 * 1024; + std::string publicId = call.GetUriComponent("id", ""); - Json::Value result; - OrthancRestApi::GetIndex(call).GetStatistics(result, publicId); + + ResourceType type; + uint64_t diskSize, uncompressedSize, dicomDiskSize, dicomUncompressedSize; + unsigned int countStudies, countSeries, countInstances; + OrthancRestApi::GetIndex(call).GetResourceStatistics( + type, diskSize, uncompressedSize, countStudies, countSeries, + countInstances, dicomDiskSize, dicomUncompressedSize, publicId); + + Json::Value result = Json::objectValue; + result["DiskSize"] = boost::lexical_cast(diskSize); + result["DiskSizeMB"] = static_cast(diskSize / MEGA_BYTES); + result["UncompressedSize"] = boost::lexical_cast(uncompressedSize); + result["UncompressedSizeMB"] = static_cast(uncompressedSize / MEGA_BYTES); + + result["DicomDiskSize"] = boost::lexical_cast(dicomDiskSize); + result["DicomDiskSizeMB"] = static_cast(dicomDiskSize / MEGA_BYTES); + result["DicomUncompressedSize"] = boost::lexical_cast(dicomUncompressedSize); + result["DicomUncompressedSizeMB"] = static_cast(dicomUncompressedSize / MEGA_BYTES); + + switch (type) + { + // Do NOT add "break" below this point! + case ResourceType_Patient: + result["CountStudies"] = countStudies; + + case ResourceType_Study: + result["CountSeries"] = countSeries; + + case ResourceType_Series: + result["CountInstances"] = countInstances; + + case ResourceType_Instance: + default: + break; + } + call.GetOutput().AnswerJson(result); } diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/OrthancRestApi/OrthancRestSystem.cpp --- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -35,6 +35,7 @@ #include "OrthancRestApi.h" #include "../../Core/DicomParsing/FromDcmtkBridge.h" +#include "../../Core/MetricsRegistry.h" #include "../../Plugins/Engine/OrthancPlugins.h" #include "../../Plugins/Engine/PluginsManager.h" #include "../OrthancConfiguration.h" @@ -93,8 +94,22 @@ static void GetStatistics(RestApiGetCall& call) { + static const uint64_t MEGA_BYTES = 1024 * 1024; + + uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; + OrthancRestApi::GetIndex(call).GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + Json::Value result = Json::objectValue; - OrthancRestApi::GetIndex(call).ComputeStatistics(result); + result["TotalDiskSize"] = boost::lexical_cast(diskSize); + result["TotalUncompressedSize"] = boost::lexical_cast(uncompressedSize); + result["TotalDiskSizeMB"] = static_cast(diskSize / MEGA_BYTES); + result["TotalUncompressedSizeMB"] = static_cast(uncompressedSize / MEGA_BYTES); + result["CountPatients"] = static_cast(countPatients); + result["CountStudies"] = static_cast(countStudies); + result["CountSeries"] = static_cast(countSeries); + result["CountInstances"] = static_cast(countInstances); + call.GetOutput().AnswerJson(result); } @@ -390,6 +405,65 @@ } + static void GetMetricsPrometheus(RestApiGetCall& call) + { + static const uint64_t MEGA_BYTES = 1024 * 1024; + + ServerContext& context = OrthancRestApi::GetContext(call); + + MetricsRegistry& registry = context.GetMetricsRegistry(); + + uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; + context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + + registry.SetValue("orthanc_disk_size_mb", static_cast(diskSize / MEGA_BYTES)); + registry.SetValue("orthanc_uncompressed_size_mb", static_cast(diskSize / MEGA_BYTES)); + registry.SetValue("orthanc_count_patients", static_cast(countPatients)); + registry.SetValue("orthanc_count_studies", static_cast(countStudies)); + registry.SetValue("orthanc_count_series", static_cast(countSeries)); + registry.SetValue("orthanc_count_instances", static_cast(countInstances)); + + std::string s; + registry.ExportPrometheusText(s); + + call.GetOutput().AnswerBuffer(s, MimeType_PrometheusText); + } + + + static void GetMetricsEnabled(RestApiGetCall& call) + { + bool enabled = OrthancRestApi::GetContext(call).GetMetricsRegistry().IsEnabled(); + call.GetOutput().AnswerBuffer(enabled ? "1" : "0", MimeType_PlainText); + } + + + static void PutMetricsEnabled(RestApiPutCall& call) + { + bool enabled; + + std::string body(call.GetBodyData()); + + if (body == "1") + { + enabled = true; + } + else if (body == "0") + { + enabled = false; + } + else + { + throw OrthancException(ErrorCode_ParameterOutOfRange, + "The HTTP body must be 0 or 1, but found: " + body); + } + + // Success + OrthancRestApi::GetContext(call).GetMetricsRegistry().SetEnabled(enabled); + call.GetOutput().AnswerBuffer("", MimeType_PlainText); + } + + void OrthancRestApi::RegisterSystem() { Register("/", ServeRoot); @@ -402,6 +476,9 @@ Register("/tools/dicom-conformance", GetDicomConformanceStatement); Register("/tools/default-encoding", GetDefaultEncoding); Register("/tools/default-encoding", SetDefaultEncoding); + Register("/tools/metrics", GetMetricsEnabled); + Register("/tools/metrics", PutMetricsEnabled); + Register("/tools/metrics-prometheus", GetMetricsPrometheus); Register("/plugins", ListPlugins); Register("/plugins/{id}", GetPlugin); diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/ServerContext.cpp --- a/OrthancServer/ServerContext.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/ServerContext.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -41,6 +41,7 @@ #include "../Core/HttpServer/HttpStreamTranscoder.h" #include "../Core/JobsEngine/SetOfInstancesJob.h" #include "../Core/Logging.h" +#include "../Core/MetricsRegistry.h" #include "../Plugins/Engine/OrthancPlugins.h" #include "OrthancConfiguration.h" @@ -237,7 +238,8 @@ #endif done_(false), haveJobsChanged_(false), - isJobsEngineUnserialized_(false) + isJobsEngineUnserialized_(false), + metricsRegistry_(new MetricsRegistry) { { OrthancConfiguration::ReaderLock lock; @@ -249,6 +251,7 @@ defaultLocalAet_ = lock.GetConfiguration().GetStringParameter("DicomAet", "ORTHANC"); jobsEngine_.SetWorkersCount(lock.GetConfiguration().GetUnsignedIntegerParameter("ConcurrentJobs", 2)); saveJobs_ = lock.GetConfiguration().GetBooleanParameter("SaveJobs", true); + metricsRegistry_->SetEnabled(lock.GetConfiguration().GetBooleanParameter("MetricsEnabled", true)); } jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200); diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/ServerContext.h --- a/OrthancServer/ServerContext.h Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/ServerContext.h Tue Jan 29 15:15:48 2019 +0100 @@ -46,6 +46,7 @@ class DicomInstanceToStore; class IStorageArea; class JobsEngine; + class MetricsRegistry; class OrthancPlugins; class ParsedDicomFile; class RestApiOutput; @@ -218,6 +219,8 @@ OrthancHttpHandler httpHandler_; bool saveJobs_; + std::auto_ptr metricsRegistry_; + public: class DicomCacheLocker : public boost::noncopyable { @@ -394,5 +397,10 @@ void SignalUpdatedModalities(); void SignalUpdatedPeers(); + + MetricsRegistry& GetMetricsRegistry() + { + return *metricsRegistry_; + } }; } diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/ServerIndex.cpp --- a/OrthancServer/ServerIndex.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/ServerIndex.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -972,24 +972,21 @@ } - void ServerIndex::ComputeStatistics(Json::Value& target) + void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize, + /* out */ uint64_t& uncompressedSize, + /* out */ uint64_t& countPatients, + /* out */ uint64_t& countStudies, + /* out */ uint64_t& countSeries, + /* out */ uint64_t& countInstances) { boost::mutex::scoped_lock lock(mutex_); - target = Json::objectValue; - - uint64_t cs = db_.GetTotalCompressedSize(); - uint64_t us = db_.GetTotalUncompressedSize(); - target["TotalDiskSize"] = boost::lexical_cast(cs); - target["TotalUncompressedSize"] = boost::lexical_cast(us); - target["TotalDiskSizeMB"] = static_cast(cs / MEGA_BYTES); - target["TotalUncompressedSizeMB"] = static_cast(us / MEGA_BYTES); - - target["CountPatients"] = static_cast(db_.GetResourceCount(ResourceType_Patient)); - target["CountStudies"] = static_cast(db_.GetResourceCount(ResourceType_Study)); - target["CountSeries"] = static_cast(db_.GetResourceCount(ResourceType_Series)); - target["CountInstances"] = static_cast(db_.GetResourceCount(ResourceType_Instance)); - } - + diskSize = db_.GetTotalCompressedSize(); + uncompressedSize = db_.GetTotalUncompressedSize(); + countPatients = db_.GetResourceCount(ResourceType_Patient); + countStudies = db_.GetResourceCount(ResourceType_Study); + countSeries = db_.GetResourceCount(ResourceType_Series); + countInstances = db_.GetResourceCount(ResourceType_Instance); + } SeriesStatus ServerIndex::GetSeriesStatus(int64_t id, @@ -1931,18 +1928,26 @@ } - void ServerIndex::GetStatisticsInternal(/* out */ uint64_t& diskSize, + void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type, + /* out */ uint64_t& diskSize, /* out */ uint64_t& uncompressedSize, /* out */ unsigned int& countStudies, /* out */ unsigned int& countSeries, /* out */ unsigned int& countInstances, /* out */ uint64_t& dicomDiskSize, /* out */ uint64_t& dicomUncompressedSize, - /* in */ int64_t id, - /* in */ ResourceType type) + const std::string& publicId) { + boost::mutex::scoped_lock lock(mutex_); + + int64_t top; + if (!db_.LookupResource(top, type, publicId)) + { + throw OrthancException(ErrorCode_UnknownResource); + } + std::stack toExplore; - toExplore.push(id); + toExplore.push(top); countInstances = 0; countSeries = 0; @@ -2023,83 +2028,6 @@ } - - void ServerIndex::GetStatistics(Json::Value& target, - const std::string& publicId) - { - boost::mutex::scoped_lock lock(mutex_); - - ResourceType type; - int64_t top; - if (!db_.LookupResource(top, type, publicId)) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - uint64_t uncompressedSize; - uint64_t diskSize; - uint64_t dicomUncompressedSize; - uint64_t dicomDiskSize; - unsigned int countStudies; - unsigned int countSeries; - unsigned int countInstances; - GetStatisticsInternal(diskSize, uncompressedSize, countStudies, - countSeries, countInstances, dicomDiskSize, dicomUncompressedSize, top, type); - - target = Json::objectValue; - target["DiskSize"] = boost::lexical_cast(diskSize); - target["DiskSizeMB"] = static_cast(diskSize / MEGA_BYTES); - target["UncompressedSize"] = boost::lexical_cast(uncompressedSize); - target["UncompressedSizeMB"] = static_cast(uncompressedSize / MEGA_BYTES); - - target["DicomDiskSize"] = boost::lexical_cast(dicomDiskSize); - target["DicomDiskSizeMB"] = static_cast(dicomDiskSize / MEGA_BYTES); - target["DicomUncompressedSize"] = boost::lexical_cast(dicomUncompressedSize); - target["DicomUncompressedSizeMB"] = static_cast(dicomUncompressedSize / MEGA_BYTES); - - switch (type) - { - // Do NOT add "break" below this point! - case ResourceType_Patient: - target["CountStudies"] = countStudies; - - case ResourceType_Study: - target["CountSeries"] = countSeries; - - case ResourceType_Series: - target["CountInstances"] = countInstances; - - case ResourceType_Instance: - default: - break; - } - } - - - void ServerIndex::GetStatistics(/* out */ uint64_t& diskSize, - /* out */ uint64_t& uncompressedSize, - /* out */ unsigned int& countStudies, - /* out */ unsigned int& countSeries, - /* out */ unsigned int& countInstances, - /* out */ uint64_t& dicomDiskSize, - /* out */ uint64_t& dicomUncompressedSize, - const std::string& publicId) - { - boost::mutex::scoped_lock lock(mutex_); - - ResourceType type; - int64_t top; - if (!db_.LookupResource(top, type, publicId)) - { - throw OrthancException(ErrorCode_UnknownResource); - } - - GetStatisticsInternal(diskSize, uncompressedSize, countStudies, - countSeries, countInstances, dicomDiskSize, - dicomUncompressedSize, top, type); - } - - void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that, unsigned int threadSleep) { diff -r 096f4a29f223 -r 8ea7c4546c3a OrthancServer/ServerIndex.h --- a/OrthancServer/ServerIndex.h Tue Jan 29 10:34:00 2019 +0100 +++ b/OrthancServer/ServerIndex.h Tue Jan 29 15:15:48 2019 +0100 @@ -97,16 +97,6 @@ Orthanc::ResourceType type, const std::string& publicId); - void GetStatisticsInternal(/* out */ uint64_t& diskSize, - /* out */ uint64_t& uncompressedSize, - /* out */ unsigned int& countStudies, - /* out */ unsigned int& countSeries, - /* out */ unsigned int& countInstances, - /* out */ uint64_t& dicomDiskSize, - /* out */ uint64_t& dicomUncompressedSize, - /* in */ int64_t id, - /* in */ ResourceType type); - bool GetMetadataAsInteger(int64_t& result, int64_t id, MetadataType type); @@ -161,7 +151,12 @@ DicomInstanceToStore& instance, const Attachments& attachments); - void ComputeStatistics(Json::Value& target); + void GetGlobalStatistics(/* out */ uint64_t& diskSize, + /* out */ uint64_t& uncompressedSize, + /* out */ uint64_t& countPatients, + /* out */ uint64_t& countStudies, + /* out */ uint64_t& countSeries, + /* out */ uint64_t& countInstances); bool LookupResource(Json::Value& result, const std::string& publicId, @@ -242,17 +237,15 @@ void DeleteExportedResources(); - void GetStatistics(Json::Value& target, - const std::string& publicId); - - void GetStatistics(/* out */ uint64_t& diskSize, - /* out */ uint64_t& uncompressedSize, - /* out */ unsigned int& countStudies, - /* out */ unsigned int& countSeries, - /* out */ unsigned int& countInstances, - /* out */ uint64_t& dicomDiskSize, - /* out */ uint64_t& dicomUncompressedSize, - const std::string& publicId); + void GetResourceStatistics(/* out */ ResourceType& type, + /* out */ uint64_t& diskSize, + /* out */ uint64_t& uncompressedSize, + /* out */ unsigned int& countStudies, + /* out */ unsigned int& countSeries, + /* out */ unsigned int& countInstances, + /* out */ uint64_t& dicomDiskSize, + /* out */ uint64_t& dicomUncompressedSize, + const std::string& publicId); void LookupIdentifierExact(std::vector& result, ResourceType level, diff -r 096f4a29f223 -r 8ea7c4546c3a Resources/CMake/OrthancFrameworkConfiguration.cmake --- a/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Jan 29 10:34:00 2019 +0100 +++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake Tue Jan 29 15:15:48 2019 +0100 @@ -532,6 +532,7 @@ list(APPEND ORTHANC_CORE_SOURCES_INTERNAL ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp + ${ORTHANC_ROOT}/Core/MetricsRegistry.cpp ${ORTHANC_ROOT}/Core/MultiThreading/RunnableWorkersPool.cpp ${ORTHANC_ROOT}/Core/MultiThreading/Semaphore.cpp ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp diff -r 096f4a29f223 -r 8ea7c4546c3a Resources/CMake/OrthancFrameworkParameters.cmake --- a/Resources/CMake/OrthancFrameworkParameters.cmake Tue Jan 29 10:34:00 2019 +0100 +++ b/Resources/CMake/OrthancFrameworkParameters.cmake Tue Jan 29 15:15:48 2019 +0100 @@ -17,7 +17,7 @@ # Version of the Orthanc API, can be retrieved from "/system" URI in # order to check whether new URI endpoints are available even if using # the mainline version of Orthanc -set(ORTHANC_API_VERSION "1.3") +set(ORTHANC_API_VERSION "1.4") ##################################################################### diff -r 096f4a29f223 -r 8ea7c4546c3a Resources/Configuration.json --- a/Resources/Configuration.json Tue Jan 29 10:34:00 2019 +0100 +++ b/Resources/Configuration.json Tue Jan 29 15:15:48 2019 +0100 @@ -476,5 +476,11 @@ // answers, but not to filter the DICOM resources (balance between // the two modes). By default, the mode is "Always", which // corresponds to the behavior of Orthanc <= 1.5.0. - "StorageAccessOnFind" : "Always" + "StorageAccessOnFind" : "Always", + + // Whether Orthanc monitors its metrics (new in Orthanc 1.5.4). If + // set to "true", the metrics can be retrieved at + // "/tools/metrics-prometheus" formetted using the Prometheus + // text-based exposition format. + "MetricsEnabled" : true } diff -r 096f4a29f223 -r 8ea7c4546c3a UnitTestsSources/ServerIndexTests.cpp --- a/UnitTestsSources/ServerIndexTests.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/UnitTestsSources/ServerIndexTests.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -702,10 +702,12 @@ index.SetMaximumStorageSize(10); - Json::Value tmp; - index.ComputeStatistics(tmp); - ASSERT_EQ(0, tmp["CountPatients"].asInt()); - ASSERT_EQ(0, boost::lexical_cast(tmp["TotalDiskSize"].asString())); + uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; + index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + + ASSERT_EQ(0u, countPatients); + ASSERT_EQ(0u, diskSize); ServerIndex::Attachments attachments; @@ -747,17 +749,19 @@ ASSERT_EQ(hasher.HashInstance(), toStore.GetHasher().HashInstance()); } - index.ComputeStatistics(tmp); - ASSERT_EQ(10, tmp["CountPatients"].asInt()); - ASSERT_EQ(0, boost::lexical_cast(tmp["TotalDiskSize"].asString())); + index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + ASSERT_EQ(10u, countPatients); + ASSERT_EQ(0u, diskSize); for (size_t i = 0; i < ids.size(); i++) { FileInfo info(Toolbox::GenerateUuid(), FileContentType_Dicom, 1, "md5"); index.AddAttachment(info, ids[i]); - index.ComputeStatistics(tmp); - ASSERT_GE(10, boost::lexical_cast(tmp["TotalDiskSize"].asString())); + index.GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + ASSERT_GE(10u, diskSize); } // Because the DB is in memory, the SQLite index must not have been created @@ -800,10 +804,12 @@ std::string id = hasher.HashInstance(); context.GetIndex().SetOverwriteInstances(overwrite); - Json::Value tmp; - context.GetIndex().ComputeStatistics(tmp); - ASSERT_EQ(0, tmp["CountInstances"].asInt()); - ASSERT_EQ(0, boost::lexical_cast(tmp["TotalDiskSize"].asString())); + uint64_t diskSize, uncompressedSize, countPatients, countStudies, countSeries, countInstances; + context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + + ASSERT_EQ(0, countInstances); + ASSERT_EQ(0, diskSize); { DicomInstanceToStore toStore; @@ -820,13 +826,13 @@ ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom1, id, FileContentType_Dicom)); ASSERT_TRUE(context.GetIndex().LookupAttachment(json1, id, FileContentType_DicomAsJson)); - context.GetIndex().ComputeStatistics(tmp); - ASSERT_EQ(1, tmp["CountInstances"].asInt()); - ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), - boost::lexical_cast(tmp["TotalDiskSize"].asString())); - ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), - boost::lexical_cast(tmp["TotalUncompressedSize"].asString())); + context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + ASSERT_EQ(1u, countInstances); + ASSERT_EQ(dicom1.GetCompressedSize() + json1.GetCompressedSize(), diskSize); + ASSERT_EQ(dicom1.GetUncompressedSize() + json1.GetUncompressedSize(), uncompressedSize); + Json::Value tmp; context.ReadDicomAsJson(tmp, id); ASSERT_EQ("name", tmp["0010,0010"]["Value"].asString()); @@ -855,12 +861,11 @@ ASSERT_TRUE(context.GetIndex().LookupAttachment(dicom2, id, FileContentType_Dicom)); ASSERT_TRUE(context.GetIndex().LookupAttachment(json2, id, FileContentType_DicomAsJson)); - context.GetIndex().ComputeStatistics(tmp); - ASSERT_EQ(1, tmp["CountInstances"].asInt()); - ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), - boost::lexical_cast(tmp["TotalDiskSize"].asString())); - ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), - boost::lexical_cast(tmp["TotalUncompressedSize"].asString())); + context.GetIndex().GetGlobalStatistics(diskSize, uncompressedSize, countPatients, + countStudies, countSeries, countInstances); + ASSERT_EQ(1, countInstances); + ASSERT_EQ(dicom2.GetCompressedSize() + json2.GetCompressedSize(), diskSize); + ASSERT_EQ(dicom2.GetUncompressedSize() + json2.GetUncompressedSize(), uncompressedSize); if (overwrite) { diff -r 096f4a29f223 -r 8ea7c4546c3a UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Tue Jan 29 10:34:00 2019 +0100 +++ b/UnitTestsSources/UnitTestsMain.cpp Tue Jan 29 15:15:48 2019 +0100 @@ -41,6 +41,7 @@ #include "../Core/DicomFormat/DicomTag.h" #include "../Core/HttpServer/HttpToolbox.h" #include "../Core/Logging.h" +#include "../Core/MetricsRegistry.h" #include "../Core/OrthancException.h" #include "../Core/TemporaryFile.h" #include "../Core/Toolbox.h" @@ -1236,6 +1237,120 @@ } +TEST(MetricsRegistry, Basic) +{ + { + MetricsRegistry m; + m.SetEnabled(false); + m.SetValue("hello.world", 42.5f); + + std::string s; + m.ExportPrometheusText(s); + ASSERT_TRUE(s.empty()); + } + + { + MetricsRegistry m; + m.Register("hello.world", MetricsType_Default); + + std::string s; + m.ExportPrometheusText(s); + ASSERT_TRUE(s.empty()); + } + + { + MetricsRegistry m; + m.SetValue("hello.world", 42.5f); + ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.world")); + ASSERT_THROW(m.GetMetricsType("nope"), OrthancException); + + std::string s; + m.ExportPrometheusText(s); + + std::vector t; + Toolbox::TokenizeString(t, s, '\n'); + ASSERT_EQ(2u, t.size()); + ASSERT_EQ("hello.world 42.5 ", t[0].substr(0, 17)); + ASSERT_TRUE(t[1].empty()); + } + + { + MetricsRegistry m; + m.Register("hello.max", MetricsType_MaxOver10Seconds); + m.SetValue("hello.max", 10); + m.SetValue("hello.max", 20); + m.SetValue("hello.max", -10); + m.SetValue("hello.max", 5); + + m.Register("hello.min", MetricsType_MinOver10Seconds); + m.SetValue("hello.min", 10); + m.SetValue("hello.min", 20); + m.SetValue("hello.min", -10); + m.SetValue("hello.min", 5); + + m.Register("hello.default", MetricsType_Default); + m.SetValue("hello.default", 10); + m.SetValue("hello.default", 20); + m.SetValue("hello.default", -10); + m.SetValue("hello.default", 5); + + ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("hello.max")); + ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("hello.min")); + ASSERT_EQ(MetricsType_Default, m.GetMetricsType("hello.default")); + + std::string s; + m.ExportPrometheusText(s); + + std::vector t; + Toolbox::TokenizeString(t, s, '\n'); + ASSERT_EQ(4u, t.size()); + ASSERT_TRUE(t[3].empty()); + + std::map u; + for (size_t i = 0; i < t.size() - 1; i++) + { + std::vector v; + Toolbox::TokenizeString(v, t[i], ' '); + u[v[0]] = v[1]; + } + + ASSERT_EQ("20", u["hello.max"]); + ASSERT_EQ("-10", u["hello.min"]); + ASSERT_EQ("5", u["hello.default"]); + } + + { + MetricsRegistry m; + + m.SetValue("a", 10); + m.SetValue("b", 10, MetricsType_MinOver10Seconds); + + m.Register("c", MetricsType_MaxOver10Seconds); + m.SetValue("c", 10, MetricsType_MinOver10Seconds); + + m.Register("d", MetricsType_MaxOver10Seconds); + m.Register("d", MetricsType_Default); + + ASSERT_EQ(MetricsType_Default, m.GetMetricsType("a")); + ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b")); + ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("c")); + ASSERT_EQ(MetricsType_Default, m.GetMetricsType("d")); + } + + { + MetricsRegistry m; + + { + MetricsRegistry::Timer t1(m, "a"); + MetricsRegistry::Timer t2(m, "b", MetricsType_MinOver10Seconds); + } + + ASSERT_EQ(MetricsType_MaxOver10Seconds, m.GetMetricsType("a")); + ASSERT_EQ(MetricsType_MinOver10Seconds, m.GetMetricsType("b")); + } +} + + int main(int argc, char **argv) { Logging::Initialize();