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());
+ }
+}
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']]]);
+ }
+ }
+}
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/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/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);
+ }
+ }
+}