From c57c4843e89120027afe23672c796a07b53028a7 Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Mon, 18 Aug 2025 16:24:23 +0200 Subject: [PATCH 1/4] feat(openmetrics): introduce OpenMetrics exporter Expose a `/metrics` endpoint with some basic metrics Signed-off-by: Benjamin Gaussorgues --- config/config.sample.php | 25 +++ core/Controller/OpenMetricsController.php | 155 ++++++++++++++++++ lib/composer/composer/LICENSE | 2 + lib/composer/composer/autoload_classmap.php | 17 ++ lib/composer/composer/autoload_static.php | 17 ++ lib/private/OpenMetrics/ExporterManager.php | 94 +++++++++++ .../OpenMetrics/Exporters/ActiveSessions.php | 58 +++++++ .../OpenMetrics/Exporters/ActiveUsers.php | 58 +++++++ .../OpenMetrics/Exporters/AppsCount.php | 62 +++++++ .../OpenMetrics/Exporters/AppsInfo.php | 56 +++++++ lib/private/OpenMetrics/Exporters/Cached.php | 58 +++++++ .../OpenMetrics/Exporters/FilesByType.php | 80 +++++++++ .../OpenMetrics/Exporters/InstanceInfo.php | 63 +++++++ .../OpenMetrics/Exporters/Maintenance.php | 50 ++++++ .../OpenMetrics/Exporters/RunningJobs.php | 67 ++++++++ .../OpenMetrics/Exporters/UsersByBackend.php | 55 +++++++ lib/private/User/Manager.php | 18 +- .../Http/StreamTraversableResponse.php | 51 ++++++ lib/public/IUserManager.php | 3 +- lib/public/OpenMetrics/IMetricFamily.php | 53 ++++++ lib/public/OpenMetrics/Metric.php | 27 +++ lib/public/OpenMetrics/MetricType.php | 26 +++ lib/public/OpenMetrics/MetricValue.php | 20 +++ openapi.json | 3 +- .../Controller/OpenMetricsControllerTest.php | 78 +++++++++ tests/lib/InfoXmlTest.php | 10 ++ tests/lib/OpenMetrics/ExporterManagerTest.php | 23 +++ .../Exporters/ActiveSessionsTest.php | 32 ++++ .../OpenMetrics/Exporters/ActiveUsersTest.php | 32 ++++ .../OpenMetrics/Exporters/AppsCountTest.php | 38 +++++ .../OpenMetrics/Exporters/AppsInfoTest.php | 37 +++++ .../Exporters/ExporterTestCase.php | 41 +++++ .../OpenMetrics/Exporters/FilesByTypeTest.php | 29 ++++ .../Exporters/InstanceInfoTest.php | 42 +++++ .../OpenMetrics/Exporters/MaintenanceTest.php | 19 +++ .../OpenMetrics/Exporters/RunningJobsTest.php | 23 +++ .../Exporters/UsersByBackendTest.php | 39 +++++ 37 files changed, 1551 insertions(+), 10 deletions(-) create mode 100644 core/Controller/OpenMetricsController.php create mode 100644 lib/private/OpenMetrics/ExporterManager.php create mode 100644 lib/private/OpenMetrics/Exporters/ActiveSessions.php create mode 100644 lib/private/OpenMetrics/Exporters/ActiveUsers.php create mode 100644 lib/private/OpenMetrics/Exporters/AppsCount.php create mode 100644 lib/private/OpenMetrics/Exporters/AppsInfo.php create mode 100644 lib/private/OpenMetrics/Exporters/Cached.php create mode 100644 lib/private/OpenMetrics/Exporters/FilesByType.php create mode 100644 lib/private/OpenMetrics/Exporters/InstanceInfo.php create mode 100644 lib/private/OpenMetrics/Exporters/Maintenance.php create mode 100644 lib/private/OpenMetrics/Exporters/RunningJobs.php create mode 100644 lib/private/OpenMetrics/Exporters/UsersByBackend.php create mode 100644 lib/public/AppFramework/Http/StreamTraversableResponse.php create mode 100644 lib/public/OpenMetrics/IMetricFamily.php create mode 100644 lib/public/OpenMetrics/Metric.php create mode 100644 lib/public/OpenMetrics/MetricType.php create mode 100644 lib/public/OpenMetrics/MetricValue.php create mode 100644 tests/Core/Controller/OpenMetricsControllerTest.php create mode 100644 tests/lib/OpenMetrics/ExporterManagerTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/ActiveSessionsTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/ActiveUsersTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/AppsCountTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/AppsInfoTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/ExporterTestCase.php create mode 100644 tests/lib/OpenMetrics/Exporters/FilesByTypeTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/InstanceInfoTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/MaintenanceTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/RunningJobsTest.php create mode 100644 tests/lib/OpenMetrics/Exporters/UsersByBackendTest.php diff --git a/config/config.sample.php b/config/config.sample.php index d36eac3ed88c1..0da5c1e6e3241 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -2892,4 +2892,29 @@ * Defaults to `\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'`. */ 'default_certificates_bundle_path' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt', + + /** + * OpenMetrics skipped exporters + * Allows to skip some exporters in the OpenMetrics endpoint ``/metrics``. + * + * Default to ``[]`` (empty array) + */ + 'openmetrics_skipped_classes' => [ + 'OC\OpenMetrics\Exporters\FilesByType', + 'OCA\Files_Sharing\OpenMetrics\SharesCount', + ], + + /** + * OpenMetrics allowed client IP addresses + * Restricts the IP addresses able to make requests on the ``/metrics`` endpoint. + * + * Keep this list as restrictive as possible as metrics can consume a lot of resources. + * + * Default to ``[127.0.0.0/16', '::1/128]`` (allow loopback interface only) + */ + 'openmetrics_allowed_clients' => [ + '192.168.0.0/16', + 'fe80::/10', + '10.0.0.1', + ], ]; diff --git a/core/Controller/OpenMetricsController.php b/core/Controller/OpenMetricsController.php new file mode 100644 index 0000000000000..58f5288531fbe --- /dev/null +++ b/core/Controller/OpenMetricsController.php @@ -0,0 +1,155 @@ +isRemoteAddressAllowed()) { + return new Http\Response(Http::STATUS_FORBIDDEN); + } + + return new Http\StreamTraversableResponse( + $this->generate(), + Http::STATUS_OK, + [ + 'Content-Type' => 'application/openmetrics-text; version=1.0.0; charset=utf-8', + ] + ); + } + + private function isRemoteAddressAllowed(): bool { + $clientAddress = new Address($this->request->getRemoteAddress()); + $allowedRanges = $this->config->getSystemValue('openmetrics_allowed_clients', ['127.0.0.0/16', '::1/128']); + if (!is_array($allowedRanges)) { + $this->logger->warning('Invalid configuration for "openmetrics_allowed_clients"'); + return false; + } + + foreach ($allowedRanges as $range) { + $range = new Range($range); + if ($range->contains($clientAddress)) { + return true; + } + } + + return false; + } + + private function generate(): \Generator { + foreach ($this->exporterManager->export() as $family) { + yield $this->formatFamily($family); + } + + $elapsed = (string)(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']); + yield <<name(); + if ($family->type() !== MetricType::unknown) { + $output = '# TYPE nextcloud_' . $name . ' ' . $family->type()->name . "\n"; + } + if ($family->unit() !== '') { + $output .= '# UNIT nextcloud_' . $name . ' ' . $family->unit() . "\n"; + } + if ($family->help() !== '') { + $output .= '# HELP nextcloud_' . $name . ' ' . $family->help() . "\n"; + } + foreach ($family->metrics() as $metric) { + $output .= 'nextcloud_' . $name . $this->formatLabels($metric) . ' ' . $this->formatValue($metric); + if ($metric->timestamp !== null) { + $output .= ' ' . $this->formatTimestamp($metric); + } + $output .= "\n"; + } + $output .= "\n"; + + return $output; + } + + private function formatLabels(Metric $metric): string { + if (empty($metric->labels)) { + return ''; + } + + $labels = []; + foreach ($metric->labels as $label => $value) { + $labels[] .= $label . '=' . $this->escapeString((string)$value); + } + + return '{' . implode(',', $labels) . '}'; + } + + private function escapeString(string $string): string { + return json_encode( + $string, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR, + 1 + ); + } + + private function formatValue(Metric $metric): string { + if (is_bool($metric->value)) { + return $metric->value ? '1' : '0'; + } + if ($metric->value instanceof MetricValue) { + return $metric->value->value; + } + + return (string)$metric->value; + } + + private function formatTimestamp(Metric $metric): string { + return (string)$metric->timestamp; + } +} diff --git a/lib/composer/composer/LICENSE b/lib/composer/composer/LICENSE index 62ecfd8d0046b..f27399a042d95 100644 --- a/lib/composer/composer/LICENSE +++ b/lib/composer/composer/LICENSE @@ -1,3 +1,4 @@ + Copyright (c) Nils Adermann, Jordi Boggiano Permission is hereby granted, free of charge, to any person obtaining a copy @@ -17,3 +18,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 7cca0c7a41455..5e9dc42f51df6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -122,6 +122,7 @@ 'OCP\\AppFramework\\Http\\Response' => $baseDir . '/lib/public/AppFramework/Http/Response.php', 'OCP\\AppFramework\\Http\\StandaloneTemplateResponse' => $baseDir . '/lib/public/AppFramework/Http/StandaloneTemplateResponse.php', 'OCP\\AppFramework\\Http\\StreamResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamResponse.php', + 'OCP\\AppFramework\\Http\\StreamTraversableResponse' => $baseDir . '/lib/public/AppFramework/Http/StreamTraversableResponse.php', 'OCP\\AppFramework\\Http\\StrictContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\StrictEvalContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\StrictInlineContentSecurityPolicy' => $baseDir . '/lib/public/AppFramework/Http/StrictInlineContentSecurityPolicy.php', @@ -724,6 +725,10 @@ 'OCP\\OCM\\IOCMProvider' => $baseDir . '/lib/public/OCM/IOCMProvider.php', 'OCP\\OCM\\IOCMResource' => $baseDir . '/lib/public/OCM/IOCMResource.php', 'OCP\\OCS\\IDiscoveryService' => $baseDir . '/lib/public/OCS/IDiscoveryService.php', + 'OCP\\OpenMetrics\\IMetricFamily' => $baseDir . '/lib/public/OpenMetrics/IMetricFamily.php', + 'OCP\\OpenMetrics\\Metric' => $baseDir . '/lib/public/OpenMetrics/Metric.php', + 'OCP\\OpenMetrics\\MetricType' => $baseDir . '/lib/public/OpenMetrics/MetricType.php', + 'OCP\\OpenMetrics\\MetricValue' => $baseDir . '/lib/public/OpenMetrics/MetricValue.php', 'OCP\\PreConditionNotMetException' => $baseDir . '/lib/public/PreConditionNotMetException.php', 'OCP\\Preview\\BeforePreviewFetchedEvent' => $baseDir . '/lib/public/Preview/BeforePreviewFetchedEvent.php', 'OCP\\Preview\\IMimeIconProvider' => $baseDir . '/lib/public/Preview/IMimeIconProvider.php', @@ -1422,6 +1427,7 @@ 'OC\\Core\\Controller\\OCJSController' => $baseDir . '/core/Controller/OCJSController.php', 'OC\\Core\\Controller\\OCMController' => $baseDir . '/core/Controller/OCMController.php', 'OC\\Core\\Controller\\OCSController' => $baseDir . '/core/Controller/OCSController.php', + 'OC\\Core\\Controller\\OpenMetricsController' => $baseDir . '/core/Controller/OpenMetricsController.php', 'OC\\Core\\Controller\\PreviewController' => $baseDir . '/core/Controller/PreviewController.php', 'OC\\Core\\Controller\\ProfileApiController' => $baseDir . '/core/Controller/ProfileApiController.php', 'OC\\Core\\Controller\\RecommendedAppsController' => $baseDir . '/core/Controller/RecommendedAppsController.php', @@ -1895,6 +1901,17 @@ 'OC\\OCS\\CoreCapabilities' => $baseDir . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\DiscoveryService' => $baseDir . '/lib/private/OCS/DiscoveryService.php', 'OC\\OCS\\Provider' => $baseDir . '/lib/private/OCS/Provider.php', + 'OC\\OpenMetrics\\ExporterManager' => $baseDir . '/lib/private/OpenMetrics/ExporterManager.php', + 'OC\\OpenMetrics\\Exporters\\ActiveSessions' => $baseDir . '/lib/private/OpenMetrics/Exporters/ActiveSessions.php', + 'OC\\OpenMetrics\\Exporters\\ActiveUsers' => $baseDir . '/lib/private/OpenMetrics/Exporters/ActiveUsers.php', + 'OC\\OpenMetrics\\Exporters\\AppsCount' => $baseDir . '/lib/private/OpenMetrics/Exporters/AppsCount.php', + 'OC\\OpenMetrics\\Exporters\\AppsInfo' => $baseDir . '/lib/private/OpenMetrics/Exporters/AppsInfo.php', + 'OC\\OpenMetrics\\Exporters\\Cached' => $baseDir . '/lib/private/OpenMetrics/Exporters/Cached.php', + 'OC\\OpenMetrics\\Exporters\\FilesByType' => $baseDir . '/lib/private/OpenMetrics/Exporters/FilesByType.php', + 'OC\\OpenMetrics\\Exporters\\InstanceInfo' => $baseDir . '/lib/private/OpenMetrics/Exporters/InstanceInfo.php', + 'OC\\OpenMetrics\\Exporters\\Maintenance' => $baseDir . '/lib/private/OpenMetrics/Exporters/Maintenance.php', + 'OC\\OpenMetrics\\Exporters\\RunningJobs' => $baseDir . '/lib/private/OpenMetrics/Exporters/RunningJobs.php', + 'OC\\OpenMetrics\\Exporters\\UsersByBackend' => $baseDir . '/lib/private/OpenMetrics/Exporters/UsersByBackend.php', 'OC\\PhoneNumberUtil' => $baseDir . '/lib/private/PhoneNumberUtil.php', 'OC\\PreviewManager' => $baseDir . '/lib/private/PreviewManager.php', 'OC\\PreviewNotAvailableException' => $baseDir . '/lib/private/PreviewNotAvailableException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index c9fcc66a253f1..3616292f589fd 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -163,6 +163,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\AppFramework\\Http\\Response' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/Response.php', 'OCP\\AppFramework\\Http\\StandaloneTemplateResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StandaloneTemplateResponse.php', 'OCP\\AppFramework\\Http\\StreamResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamResponse.php', + 'OCP\\AppFramework\\Http\\StreamTraversableResponse' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StreamTraversableResponse.php', 'OCP\\AppFramework\\Http\\StrictContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StrictContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\StrictEvalContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StrictEvalContentSecurityPolicy.php', 'OCP\\AppFramework\\Http\\StrictInlineContentSecurityPolicy' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Http/StrictInlineContentSecurityPolicy.php', @@ -765,6 +766,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\OCM\\IOCMProvider' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMProvider.php', 'OCP\\OCM\\IOCMResource' => __DIR__ . '/../../..' . '/lib/public/OCM/IOCMResource.php', 'OCP\\OCS\\IDiscoveryService' => __DIR__ . '/../../..' . '/lib/public/OCS/IDiscoveryService.php', + 'OCP\\OpenMetrics\\IMetricFamily' => __DIR__ . '/../../..' . '/lib/public/OpenMetrics/IMetricFamily.php', + 'OCP\\OpenMetrics\\Metric' => __DIR__ . '/../../..' . '/lib/public/OpenMetrics/Metric.php', + 'OCP\\OpenMetrics\\MetricType' => __DIR__ . '/../../..' . '/lib/public/OpenMetrics/MetricType.php', + 'OCP\\OpenMetrics\\MetricValue' => __DIR__ . '/../../..' . '/lib/public/OpenMetrics/MetricValue.php', 'OCP\\PreConditionNotMetException' => __DIR__ . '/../../..' . '/lib/public/PreConditionNotMetException.php', 'OCP\\Preview\\BeforePreviewFetchedEvent' => __DIR__ . '/../../..' . '/lib/public/Preview/BeforePreviewFetchedEvent.php', 'OCP\\Preview\\IMimeIconProvider' => __DIR__ . '/../../..' . '/lib/public/Preview/IMimeIconProvider.php', @@ -1463,6 +1468,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Controller\\OCJSController' => __DIR__ . '/../../..' . '/core/Controller/OCJSController.php', 'OC\\Core\\Controller\\OCMController' => __DIR__ . '/../../..' . '/core/Controller/OCMController.php', 'OC\\Core\\Controller\\OCSController' => __DIR__ . '/../../..' . '/core/Controller/OCSController.php', + 'OC\\Core\\Controller\\OpenMetricsController' => __DIR__ . '/../../..' . '/core/Controller/OpenMetricsController.php', 'OC\\Core\\Controller\\PreviewController' => __DIR__ . '/../../..' . '/core/Controller/PreviewController.php', 'OC\\Core\\Controller\\ProfileApiController' => __DIR__ . '/../../..' . '/core/Controller/ProfileApiController.php', 'OC\\Core\\Controller\\RecommendedAppsController' => __DIR__ . '/../../..' . '/core/Controller/RecommendedAppsController.php', @@ -1936,6 +1942,17 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\OCS\\CoreCapabilities' => __DIR__ . '/../../..' . '/lib/private/OCS/CoreCapabilities.php', 'OC\\OCS\\DiscoveryService' => __DIR__ . '/../../..' . '/lib/private/OCS/DiscoveryService.php', 'OC\\OCS\\Provider' => __DIR__ . '/../../..' . '/lib/private/OCS/Provider.php', + 'OC\\OpenMetrics\\ExporterManager' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/ExporterManager.php', + 'OC\\OpenMetrics\\Exporters\\ActiveSessions' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/ActiveSessions.php', + 'OC\\OpenMetrics\\Exporters\\ActiveUsers' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/ActiveUsers.php', + 'OC\\OpenMetrics\\Exporters\\AppsCount' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/AppsCount.php', + 'OC\\OpenMetrics\\Exporters\\AppsInfo' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/AppsInfo.php', + 'OC\\OpenMetrics\\Exporters\\Cached' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/Cached.php', + 'OC\\OpenMetrics\\Exporters\\FilesByType' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/FilesByType.php', + 'OC\\OpenMetrics\\Exporters\\InstanceInfo' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/InstanceInfo.php', + 'OC\\OpenMetrics\\Exporters\\Maintenance' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/Maintenance.php', + 'OC\\OpenMetrics\\Exporters\\RunningJobs' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/RunningJobs.php', + 'OC\\OpenMetrics\\Exporters\\UsersByBackend' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporters/UsersByBackend.php', 'OC\\PhoneNumberUtil' => __DIR__ . '/../../..' . '/lib/private/PhoneNumberUtil.php', 'OC\\PreviewManager' => __DIR__ . '/../../..' . '/lib/private/PreviewManager.php', 'OC\\PreviewNotAvailableException' => __DIR__ . '/../../..' . '/lib/private/PreviewNotAvailableException.php', diff --git a/lib/private/OpenMetrics/ExporterManager.php b/lib/private/OpenMetrics/ExporterManager.php new file mode 100644 index 0000000000000..c66846be49224 --- /dev/null +++ b/lib/private/OpenMetrics/ExporterManager.php @@ -0,0 +1,94 @@ +skippedClasses = array_fill_keys($config->getSystemValue('openmetrics_skipped_classes', []), true); + } + + public function export(): Generator { + // Core exporters + $exporters = [ + // Basic exporters + Exporters\InstanceInfo::class, + Exporters\AppsInfo::class, + Exporters\AppsCount::class, + Exporters\Maintenance::class, + + // File exporters + Exporters\FilesByType::class, + + // Users exporters + Exporters\ActiveUsers::class, + Exporters\ActiveSessions::class, + Exporters\UsersByBackend::class, + + // Jobs + Exporters\RunningJobs::class, + ]; + $exporters = array_filter($exporters, fn ($classname) => !isset($this->skippedClasses[$classname])); + foreach ($exporters as $classname) { + $exporter = $this->loadExporter($classname); + if ($exporter !== null) { + yield $exporter; + } + } + + // Apps exporters + foreach ($this->appManager->getEnabledApps() as $appId) { + $appInfo = $this->appManager->getAppInfo($appId); + if (!isset($appInfo[self::XML_ENTRY]) || !is_array($appInfo[self::XML_ENTRY])) { + continue; + } + foreach ($appInfo[self::XML_ENTRY] as $classname) { + if (isset($this->skippedClasses[$classname])) { + continue; + } + $exporter = $this->loadExporter($classname, $appId); + if ($exporter !== null) { + yield $exporter; + } + } + } + } + + private function loadExporter(string $classname, string $appId = 'core'): ?IMetricFamily { + try { + return Server::get($classname); + } catch (\Exception $e) { + $this->logger->error( + 'Unable to build exporter {exporter}', + [ + 'app' => $appId, + 'exception' => $e, + 'exporter' => $classname, + ], + ); + } + + return null; + } +} diff --git a/lib/private/OpenMetrics/Exporters/ActiveSessions.php b/lib/private/OpenMetrics/Exporters/ActiveSessions.php new file mode 100644 index 0000000000000..b95279fa83ecc --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/ActiveSessions.php @@ -0,0 +1,58 @@ + $now - 5 * 60, + 'Last 15 minutes' => $now - 15 * 60, + 'Last hour' => $now - 60 * 60, + 'Last day' => $now - 24 * 60 * 60, + ]; + foreach ($timeFrames as $label => $time) { + $queryBuilder = $this->connection->getQueryBuilder(); + $result = $queryBuilder->select($queryBuilder->func()->count('*')) + ->from('authtoken') + ->where($queryBuilder->expr()->gte('last_activity', $queryBuilder->createNamedParameter($time))) + ->executeQuery(); + + yield new Metric((int)$result->fetchOne(), ['time' => $label]); + } + } +} diff --git a/lib/private/OpenMetrics/Exporters/ActiveUsers.php b/lib/private/OpenMetrics/Exporters/ActiveUsers.php new file mode 100644 index 0000000000000..43425fd37e20f --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/ActiveUsers.php @@ -0,0 +1,58 @@ + $now - 5 * 60, + 'Last 15 minutes' => $now - 15 * 60, + 'Last hour' => $now - 60 * 60, + 'Last day' => $now - 24 * 60 * 60, + ]; + foreach ($timeFrames as $label => $time) { + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select($qb->createFunction('COUNT(DISTINCT ' . $qb->getColumnName('uid') . ')')) + ->from('authtoken') + ->where($qb->expr()->gte('last_activity', $qb->createNamedParameter($time))) + ->executeQuery(); + + yield new Metric((int)$result->fetchOne(), ['time' => $label]); + } + } +} diff --git a/lib/private/OpenMetrics/Exporters/AppsCount.php b/lib/private/OpenMetrics/Exporters/AppsCount.php new file mode 100644 index 0000000000000..c168d33f005e1 --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/AppsCount.php @@ -0,0 +1,62 @@ +appManager->getAppInstalledVersions(false)); + $enabledAppsCount = count($this->appManager->getEnabledApps()); + $disabledAppsCount = $installedAppsCount - $enabledAppsCount; + yield new Metric( + $disabledAppsCount, + ['status' => 'disabled'], + ); + yield new Metric( + $enabledAppsCount, + ['status' => 'enabled'], + ); + } +} diff --git a/lib/private/OpenMetrics/Exporters/AppsInfo.php b/lib/private/OpenMetrics/Exporters/AppsInfo.php new file mode 100644 index 0000000000000..33597795e7d51 --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/AppsInfo.php @@ -0,0 +1,56 @@ +appManager->getAppInstalledVersions(true), + time() + ); + } +} diff --git a/lib/private/OpenMetrics/Exporters/Cached.php b/lib/private/OpenMetrics/Exporters/Cached.php new file mode 100644 index 0000000000000..9b0a8b3868708 --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/Cached.php @@ -0,0 +1,58 @@ +cache = $cacheFactory->createDistributed('openmetrics'); + } + + /** + * Number of seconds to keep the results + */ + abstract public function getTTL(): int; + + /** + * Actually gather the metrics + * + * @see metrics + */ + abstract public function gatherMetrics(): Generator; + + #[Override] + public function metrics(): Generator { + $cacheKey = static::class; + if ($data = $this->cache->get($cacheKey)) { + yield from unserialize($data); + return; + } + + $data = []; + foreach ($this->gatherMetrics() as $metric) { + yield $metric; + $data[] = $metric; + } + + $this->cache->set($cacheKey, serialize($data), $this->getTTL()); + } +} diff --git a/lib/private/OpenMetrics/Exporters/FilesByType.php b/lib/private/OpenMetrics/Exporters/FilesByType.php new file mode 100644 index 0000000000000..a201ad70affec --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/FilesByType.php @@ -0,0 +1,80 @@ +connection->getQueryBuilder()->runAcrossAllShards(); + $metrics = $qb->select('mimetype', $qb->func()->count('*', 'count')) + ->from('filecache') + ->groupBy('mimetype') + ->executeQuery(); + + if ($metrics->rowCount() === 0) { + yield new Metric(0); + return; + } + $now = time(); + foreach ($metrics->iterateAssociative() as $count) { + yield new Metric( + $count['count'], + ['mimetype' => $this->mimetypeLoader->getMimetypeById($count['mimetype'])], + $now, + ); + } + } +} diff --git a/lib/private/OpenMetrics/Exporters/InstanceInfo.php b/lib/private/OpenMetrics/Exporters/InstanceInfo.php new file mode 100644 index 0000000000000..7b263efc169d2 --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/InstanceInfo.php @@ -0,0 +1,63 @@ + $this->serverVersion->getHumanVersion(), + 'major version' => (string)$this->serverVersion->getVersion()[0], + 'build' => $this->serverVersion->getBuild(), + 'installed' => $this->systemConfig->getValue('installed', false) ? '1' : '0', + ], + time() + ); + } +} diff --git a/lib/private/OpenMetrics/Exporters/Maintenance.php b/lib/private/OpenMetrics/Exporters/Maintenance.php new file mode 100644 index 0000000000000..89b53547e3363 --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/Maintenance.php @@ -0,0 +1,50 @@ +getValue('maintenance', false) + ); + } +} diff --git a/lib/private/OpenMetrics/Exporters/RunningJobs.php b/lib/private/OpenMetrics/Exporters/RunningJobs.php new file mode 100644 index 0000000000000..98c901c834293 --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/RunningJobs.php @@ -0,0 +1,67 @@ +connection->getQueryBuilder(); + $result = $qb->select($qb->func()->count('*', 'nb'), 'class') + ->from('jobs') + ->where($qb->expr()->gt('reserved_at', $qb->createNamedParameter(0))) + ->groupBy('class') + ->executeQuery(); + + // If no result, return a metric with count '0' + if ($result->rowCount() === 0) { + yield new Metric(0); + return; + } + + foreach ($result->iterateAssociative() as $row) { + yield new Metric($row['nb'], ['class' => $row['class']]); + } + } +} diff --git a/lib/private/OpenMetrics/Exporters/UsersByBackend.php b/lib/private/OpenMetrics/Exporters/UsersByBackend.php new file mode 100644 index 0000000000000..a31563577451a --- /dev/null +++ b/lib/private/OpenMetrics/Exporters/UsersByBackend.php @@ -0,0 +1,55 @@ +userManager->countUsers(true); + foreach ($userCounts as $backend => $count) { + yield new Metric($count, ['backend' => $backend]); + } + } +} diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 4a42a397a8e47..2bea9bb65dd74 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -443,22 +443,23 @@ public function createUserFromBackend($uid, $password, UserInterface $backend) { /** * returns how many users per backend exist (if supported by backend) * - * @param boolean $hasLoggedIn when true only users that have a lastLogin - * entry in the preferences table will be affected * @return array an array of backend class as key and count number as value */ - public function countUsers() { + public function countUsers(bool $onlyMappedUsers = false) { $userCountStatistics = []; foreach ($this->backends as $backend) { + $name = $backend instanceof IUserBackend + ? $backend->getBackendName() + : get_class($backend); + + if ($onlyMappedUsers && $backend instanceof ICountMappedUsersBackend) { + $userCountStatistics[$name] = $backend->countMappedUsers(); + continue; + } if ($backend instanceof ICountUsersBackend || $backend->implementsActions(Backend::COUNT_USERS)) { /** @var ICountUsersBackend|IUserBackend $backend */ $backendUsers = $backend->countUsers(); if ($backendUsers !== false) { - if ($backend instanceof IUserBackend) { - $name = $backend->getBackendName(); - } else { - $name = get_class($backend); - } if (isset($userCountStatistics[$name])) { $userCountStatistics[$name] += $backendUsers; } else { @@ -467,6 +468,7 @@ public function countUsers() { } } } + return $userCountStatistics; } diff --git a/lib/public/AppFramework/Http/StreamTraversableResponse.php b/lib/public/AppFramework/Http/StreamTraversableResponse.php new file mode 100644 index 0000000000000..aecf57c80596f --- /dev/null +++ b/lib/public/AppFramework/Http/StreamTraversableResponse.php @@ -0,0 +1,51 @@ + + * @template-extends Response> + */ +class StreamTraversableResponse extends Response implements ICallbackResponse { + /** + * @param S $status + * @param H $headers + * @since 33.0.0 + */ + public function __construct( + private Traversable $generator, + int $status = Http::STATUS_OK, + array $headers = [], + ) { + parent::__construct($status, $headers); + } + + + /** + * Streams the generator output + * + * @param IOutput $output a small wrapper that handles output + * @since 33.0.0 + */ + #[Override] + public function callback(IOutput $output): void { + foreach ($this->generator as $content) { + $output->setOutput($content); + flush(); + } + } +} diff --git a/lib/public/IUserManager.php b/lib/public/IUserManager.php index 226a52809a3d0..caf1a704cce99 100644 --- a/lib/public/IUserManager.php +++ b/lib/public/IUserManager.php @@ -162,8 +162,9 @@ public function createUserFromBackend($uid, $password, UserInterface $backend); * * @return array an array of backend class name as key and count number as value * @since 8.0.0 + * @since 33.0.0 $onlyMappedUsers parameter */ - public function countUsers(); + public function countUsers(bool $onlyMappedUsers = false); /** * Get how many users exists in total, whithin limit diff --git a/lib/public/OpenMetrics/IMetricFamily.php b/lib/public/OpenMetrics/IMetricFamily.php new file mode 100644 index 0000000000000..2fcc9c5d4505e --- /dev/null +++ b/lib/public/OpenMetrics/IMetricFamily.php @@ -0,0 +1,53 @@ + + * @since 33.0.0 + */ + public function metrics(): Generator; +} diff --git a/lib/public/OpenMetrics/Metric.php b/lib/public/OpenMetrics/Metric.php new file mode 100644 index 0000000000000..47e925b01b75f --- /dev/null +++ b/lib/public/OpenMetrics/Metric.php @@ -0,0 +1,27 @@ +labels[$name] ?? null; + } +} diff --git a/lib/public/OpenMetrics/MetricType.php b/lib/public/OpenMetrics/MetricType.php new file mode 100644 index 0000000000000..07449593cc007 --- /dev/null +++ b/lib/public/OpenMetrics/MetricType.php @@ -0,0 +1,26 @@ +request = $this->createMock(IRequest::class); + $this->request->method('getRemoteAddress') + ->willReturn('192.168.1.1'); + $this->config = $this->createMock(IConfig::class); + $this->exporterManager = $this->createMock(ExporterManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->controller = new OpenMetricsController('core', $this->request, $this->config, $this->exporterManager, $this->logger); + } + + public function testGetMetrics(): void { + $output = $this->createMock(IOutput::class); + $fullOutput = ''; + $output->method('setOutput') + ->willReturnCallback(function ($output) use (&$fullOutput) { + $fullOutput .= $output; + }); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('openmetrics_allowed_clients') + ->willReturn(['192.168.0.0/16']); + $response = $this->controller->export(); + $this->assertInstanceOf(StreamTraversableResponse::class, $response); + $this->assertEquals('200', $response->getStatus()); + $this->assertEquals('application/openmetrics-text; version=1.0.0; charset=utf-8', $response->getHeaders()['Content-Type']); + $expected = <<callback($output); + $this->assertStringMatchesFormat($expected, $fullOutput); + } + + public function testGetMetricsFromForbiddenIp(): void { + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('openmetrics_allowed_clients') + ->willReturn(['1.2.3.4']); + $response = $this->controller->export(); + $this->assertInstanceOf(Response::class, $response); + $this->assertEquals('403', $response->getStatus()); + } +} diff --git a/tests/lib/InfoXmlTest.php b/tests/lib/InfoXmlTest.php index 9506f87c1b0a0..4e2620439d23c 100644 --- a/tests/lib/InfoXmlTest.php +++ b/tests/lib/InfoXmlTest.php @@ -9,6 +9,7 @@ use OCP\App\IAppManager; use OCP\AppFramework\App; +use OCP\OpenMetrics\IMetricFamily; use OCP\Server; /** @@ -130,5 +131,14 @@ public function testClasses($app): void { $this->assertInstanceOf($command, Server::get($command)); } } + + if (isset($appInfo['openmetrics'])) { + foreach ($appInfo['openmetrics'] as $class) { + $this->assertTrue(class_exists($class), 'Asserting exporter "' . $class . '"exists'); + $exporter = Server::get($class); + $this->assertInstanceOf($class, $exporter); + $this->assertInstanceOf(IMetricFamily::class, $exporter); + } + } } } diff --git a/tests/lib/OpenMetrics/ExporterManagerTest.php b/tests/lib/OpenMetrics/ExporterManagerTest.php new file mode 100644 index 0000000000000..31f407ee70562 --- /dev/null +++ b/tests/lib/OpenMetrics/ExporterManagerTest.php @@ -0,0 +1,23 @@ +assertInstanceOf(ExporterManager::class, $exporter); + foreach ($exporter->export() as $metric) { + $this->assertInstanceOf(IMetricFamily::class, $metric); + }; + } +} diff --git a/tests/lib/OpenMetrics/Exporters/ActiveSessionsTest.php b/tests/lib/OpenMetrics/Exporters/ActiveSessionsTest.php new file mode 100644 index 0000000000000..ebbe3b061d3cc --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/ActiveSessionsTest.php @@ -0,0 +1,32 @@ +assertLabelsAre([ + ['time' => 'Last 5 minutes'], + ['time' => 'Last 15 minutes'], + ['time' => 'Last hour'], + ['time' => 'Last day'], + ]); + } +} diff --git a/tests/lib/OpenMetrics/Exporters/ActiveUsersTest.php b/tests/lib/OpenMetrics/Exporters/ActiveUsersTest.php new file mode 100644 index 0000000000000..1a41ee2a43b0b --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/ActiveUsersTest.php @@ -0,0 +1,32 @@ +assertLabelsAre([ + ['time' => 'Last 5 minutes'], + ['time' => 'Last 15 minutes'], + ['time' => 'Last hour'], + ['time' => 'Last day'], + ]); + } +} diff --git a/tests/lib/OpenMetrics/Exporters/AppsCountTest.php b/tests/lib/OpenMetrics/Exporters/AppsCountTest.php new file mode 100644 index 0000000000000..c1617e1ba1db4 --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/AppsCountTest.php @@ -0,0 +1,38 @@ +appManager = $this->createMock(IAppManager::class); + $this->appManager->method('getAppInstalledVersions') + ->with(false) + ->willReturn(['app1', 'app2', 'app3', 'app4', 'app5']); + $this->appManager->method('getEnabledApps') + ->willReturn(['app1', 'app2', 'app3']); + return new AppsCount($this->appManager); + } + + public function testMetrics(): void { + foreach ($this->metrics as $metric) { + $expectedValue = match ($metric->label('status')) { + 'disabled' => 2, + 'enabled' => 3, + }; + $this->assertEquals($expectedValue, $metric->value); + } + } +} diff --git a/tests/lib/OpenMetrics/Exporters/AppsInfoTest.php b/tests/lib/OpenMetrics/Exporters/AppsInfoTest.php new file mode 100644 index 0000000000000..5baa85af3f9f3 --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/AppsInfoTest.php @@ -0,0 +1,37 @@ + '0.1.2', + 'appB' => '1.2.3 beta 4', + ]; + + protected function getExporter():IMetricFamily { + $this->appManager = $this->createMock(IAppManager::class); + $this->appManager->method('getAppInstalledVersions') + ->with(true) + ->willReturn($this->appList); + + return new AppsInfo($this->appManager); + } + + public function testMetrics(): void { + $this->assertCount(1, $this->metrics); + $metric = array_pop($this->metrics); + $this->assertSame($this->appList, $metric->labels); + } +} diff --git a/tests/lib/OpenMetrics/Exporters/ExporterTestCase.php b/tests/lib/OpenMetrics/Exporters/ExporterTestCase.php new file mode 100644 index 0000000000000..cf7ffbdab1815 --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/ExporterTestCase.php @@ -0,0 +1,41 @@ +exporter = $this->getExporter(); + $this->metrics = iterator_to_array($this->exporter->metrics()); + } + + public function testNotEmptyData() { + $this->assertNotEmpty($this->exporter->name()); + $this->assertNotEmpty($this->metrics); + } + + protected function assertLabelsAre(array $expectedLabels) { + $foundLabels = []; + foreach ($this->metrics as $metric) { + $foundLabels[] = $metric->labels; + } + + $this->assertSame($foundLabels, $expectedLabels); + } +} diff --git a/tests/lib/OpenMetrics/Exporters/FilesByTypeTest.php b/tests/lib/OpenMetrics/Exporters/FilesByTypeTest.php new file mode 100644 index 0000000000000..030f9caf9a0aa --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/FilesByTypeTest.php @@ -0,0 +1,29 @@ +systemConfig = $this->createMock(SystemConfig::class); + $this->serverVersion = $this->createMock(ServerVersion::class); + $this->serverVersion->method('getHumanVersion')->willReturn('33.13.17 Gold'); + $this->serverVersion->method('getVersion')->willReturn([33, 13, 17]); + $this->serverVersion->method('getBuild')->willReturn('dev'); + + return new InstanceInfo($this->systemConfig, $this->serverVersion); + } + + public function testMetrics(): void { + $this->assertCount(1, $this->metrics); + $metric = array_pop($this->metrics); + $this->assertSame([ + 'full version' => '33.13.17 Gold', + 'major version' => '33', + 'build' => 'dev', + 'installed' => '0', + ], $metric->labels); + } +} diff --git a/tests/lib/OpenMetrics/Exporters/MaintenanceTest.php b/tests/lib/OpenMetrics/Exporters/MaintenanceTest.php new file mode 100644 index 0000000000000..5509c318ecae0 --- /dev/null +++ b/tests/lib/OpenMetrics/Exporters/MaintenanceTest.php @@ -0,0 +1,19 @@ + 42, + 'backend B' => 51, + 'backend C' => 0, + ]; + + + protected function getExporter():IMetricFamily { + $this->userManager = $this->createMock(IUserManager::class); + $this->userManager->method('countUsers') + ->with(true) + ->willReturn($this->backendList); + return new UsersByBackend($this->userManager); + } + + public function testMetrics(): void { + foreach ($this->metrics as $metric) { + $this->assertEquals($this->backendList[$metric->label('backend')], $metric->value); + } + } +} From 47de1649462818ee0a038aea9ed2233eb5e1cda4 Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Thu, 21 Aug 2025 10:52:03 +0200 Subject: [PATCH 2/4] feat(comments): add basic OpenMetrics exporter Signed-off-by: Benjamin Gaussorgues --- apps/comments/appinfo/info.xml | 4 ++ .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/OpenMetrics/CommentsCountMetric.php | 52 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 apps/comments/lib/OpenMetrics/CommentsCountMetric.php diff --git a/apps/comments/appinfo/info.xml b/apps/comments/appinfo/info.xml index 956a8fe39142e..efab11b68d876 100644 --- a/apps/comments/appinfo/info.xml +++ b/apps/comments/appinfo/info.xml @@ -38,6 +38,10 @@ + + OCA\Comments\OpenMetrics\CommentsCountMetric + + OCA\Comments\Collaboration\CommentersSorter diff --git a/apps/comments/composer/composer/autoload_classmap.php b/apps/comments/composer/composer/autoload_classmap.php index 6db5c6a232b6a..a6cd74eeaaa89 100644 --- a/apps/comments/composer/composer/autoload_classmap.php +++ b/apps/comments/composer/composer/autoload_classmap.php @@ -22,5 +22,6 @@ 'OCA\\Comments\\MaxAutoCompleteResultsInitialState' => $baseDir . '/../lib/MaxAutoCompleteResultsInitialState.php', 'OCA\\Comments\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php', 'OCA\\Comments\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', + 'OCA\\Comments\\OpenMetrics\\CommentsCountMetric' => $baseDir . '/../lib/OpenMetrics/CommentsCountMetric.php', 'OCA\\Comments\\Search\\CommentsSearchProvider' => $baseDir . '/../lib/Search/CommentsSearchProvider.php', ); diff --git a/apps/comments/composer/composer/autoload_static.php b/apps/comments/composer/composer/autoload_static.php index 60359abb6d0e6..6dee6b1fc969d 100644 --- a/apps/comments/composer/composer/autoload_static.php +++ b/apps/comments/composer/composer/autoload_static.php @@ -37,6 +37,7 @@ class ComposerStaticInitComments 'OCA\\Comments\\MaxAutoCompleteResultsInitialState' => __DIR__ . '/..' . '/../lib/MaxAutoCompleteResultsInitialState.php', 'OCA\\Comments\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php', 'OCA\\Comments\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', + 'OCA\\Comments\\OpenMetrics\\CommentsCountMetric' => __DIR__ . '/..' . '/../lib/OpenMetrics/CommentsCountMetric.php', 'OCA\\Comments\\Search\\CommentsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/CommentsSearchProvider.php', ); diff --git a/apps/comments/lib/OpenMetrics/CommentsCountMetric.php b/apps/comments/lib/OpenMetrics/CommentsCountMetric.php new file mode 100644 index 0000000000000..d0c9233b7c314 --- /dev/null +++ b/apps/comments/lib/OpenMetrics/CommentsCountMetric.php @@ -0,0 +1,52 @@ +connection->getQueryBuilder(); + $result = $qb->select($qb->func()->count()) + ->from('comments') + ->where($qb->expr()->eq('verb', $qb->expr()->literal('comment'))) + ->executeQuery(); + + yield new Metric($result->fetchOne(), [], time()); + } +} From 6ee8325b3e10c235be629a8d45ac16b186e3267b Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Thu, 21 Aug 2025 10:52:49 +0200 Subject: [PATCH 3/4] feat(files_sharing): add basic OpenMetrics exporter for files shares Signed-off-by: Benjamin Gaussorgues --- apps/files_sharing/appinfo/info.xml | 4 + .../composer/composer/autoload_classmap.php | 1 + .../composer/composer/autoload_static.php | 1 + .../lib/OpenMetrics/SharesCountMetric.php | 75 +++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 apps/files_sharing/lib/OpenMetrics/SharesCountMetric.php diff --git a/apps/files_sharing/appinfo/info.xml b/apps/files_sharing/appinfo/info.xml index 9368225fa2436..8eaacf1cb2025 100644 --- a/apps/files_sharing/appinfo/info.xml +++ b/apps/files_sharing/appinfo/info.xml @@ -87,4 +87,8 @@ Turning the feature off removes shared files and folders on the server for all s public.php + + + OCA\Files_Sharing\OpenMetrics\SharesCountMetric + diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index 48f197f9bf937..919241be141df 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -88,6 +88,7 @@ 'OCA\\Files_Sharing\\MountProvider' => $baseDir . '/../lib/MountProvider.php', 'OCA\\Files_Sharing\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php', 'OCA\\Files_Sharing\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', + 'OCA\\Files_Sharing\\OpenMetrics\\SharesCountMetric' => $baseDir . '/../lib/OpenMetrics/SharesCountMetric.php', 'OCA\\Files_Sharing\\OrphanHelper' => $baseDir . '/../lib/OrphanHelper.php', 'OCA\\Files_Sharing\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php', 'OCA\\Files_Sharing\\Scanner' => $baseDir . '/../lib/Scanner.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 110a64fb3acfa..6a22d082df14b 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -103,6 +103,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\MountProvider' => __DIR__ . '/..' . '/../lib/MountProvider.php', 'OCA\\Files_Sharing\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php', 'OCA\\Files_Sharing\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', + 'OCA\\Files_Sharing\\OpenMetrics\\SharesCountMetric' => __DIR__ . '/..' . '/../lib/OpenMetrics/SharesCountMetric.php', 'OCA\\Files_Sharing\\OrphanHelper' => __DIR__ . '/..' . '/../lib/OrphanHelper.php', 'OCA\\Files_Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php', 'OCA\\Files_Sharing\\Scanner' => __DIR__ . '/..' . '/../lib/Scanner.php', diff --git a/apps/files_sharing/lib/OpenMetrics/SharesCountMetric.php b/apps/files_sharing/lib/OpenMetrics/SharesCountMetric.php new file mode 100644 index 0000000000000..ebf7972c3af1a --- /dev/null +++ b/apps/files_sharing/lib/OpenMetrics/SharesCountMetric.php @@ -0,0 +1,75 @@ + 'user', + IShare::TYPE_GROUP => 'group', + IShare::TYPE_LINK => 'link', + IShare::TYPE_EMAIL => 'email', + ]; + $qb = $this->connection->getQueryBuilder(); + $result = $qb->select($qb->func()->count('*', 'count'), 'share_type') + ->from('share') + ->where($qb->expr()->in('share_type', $qb->createNamedParameter(array_keys($types), IQueryBuilder::PARAM_INT_ARRAY))) + ->groupBy('share_type') + ->executeQuery(); + + if ($result->rowCount() === 0) { + yield new Metric(0); + return; + } + + foreach ($result->iterateAssociative() as $row) { + yield new Metric($row['count'], ['type' => $types[$row['share_type']]]); + } + } +} From 71fa5937b2c285d3221e1db1640e99ed33f0924b Mon Sep 17 00:00:00 2001 From: Benjamin Gaussorgues Date: Thu, 18 Dec 2025 11:12:23 +0100 Subject: [PATCH 4/4] feat(openapi): add OpenMetrics controller into OpenAPI Signed-off-by: Benjamin Gaussorgues --- core/openapi-administration.json | 4 ++++ core/openapi-ex_app.json | 4 ++++ core/openapi-full.json | 4 ++++ core/openapi.json | 4 ++++ openapi.json | 7 +++++-- 5 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/openapi-administration.json b/core/openapi-administration.json index f482f4992c131..569b6f9e42184 100644 --- a/core/openapi-administration.json +++ b/core/openapi-administration.json @@ -659,6 +659,10 @@ { "name": "ocm", "description": "Controller about the endpoint /ocm-provider/" + }, + { + "name": "open_metrics", + "description": "OpenMetrics controller Gather and display metrics" } ] } diff --git a/core/openapi-ex_app.json b/core/openapi-ex_app.json index 647c9fcd5d916..927a7d788f6b3 100644 --- a/core/openapi-ex_app.json +++ b/core/openapi-ex_app.json @@ -1609,6 +1609,10 @@ { "name": "ocm", "description": "Controller about the endpoint /ocm-provider/" + }, + { + "name": "open_metrics", + "description": "OpenMetrics controller Gather and display metrics" } ] } diff --git a/core/openapi-full.json b/core/openapi-full.json index 2116be274a863..e6e456e244289 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -12238,6 +12238,10 @@ { "name": "ocm", "description": "Controller about the endpoint /ocm-provider/" + }, + { + "name": "open_metrics", + "description": "OpenMetrics controller Gather and display metrics" } ] } diff --git a/core/openapi.json b/core/openapi.json index 8b2b725d5769c..11d0318c837cc 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -10379,6 +10379,10 @@ { "name": "ocm", "description": "Controller about the endpoint /ocm-provider/" + }, + { + "name": "open_metrics", + "description": "OpenMetrics controller Gather and display metrics" } ] } diff --git a/openapi.json b/openapi.json index 7aff72d29a187..e8158ad20c8bc 100644 --- a/openapi.json +++ b/openapi.json @@ -33,6 +33,10 @@ "name": "core/ocm", "description": "Controller about the endpoint /ocm-provider/" }, + { + "name": "core/open_metrics", + "description": "OpenMetrics controller Gather and display metrics" + }, { "name": "cloud_federation_api/request_handler", "description": "Open-Cloud-Mesh-API" @@ -16970,8 +16974,7 @@ "schema": { "type": "object", "required": [ - "fileId", - "expirationTime" + "fileId" ], "properties": { "fileId": {