diff --git a/apps/comments/appinfo/info.xml b/apps/comments/appinfo/info.xml
index 956a8fe39142e..7510bbdf53dfb 100644
--- a/apps/comments/appinfo/info.xml
+++ b/apps/comments/appinfo/info.xml
@@ -38,6 +38,10 @@
+
+ OCA\Comments\OpenMetrics\Comments
+
+
OCA\Comments\Collaboration\CommentersSorter
diff --git a/apps/comments/composer/composer/autoload_classmap.php b/apps/comments/composer/composer/autoload_classmap.php
index 6db5c6a232b6a..22ed3a35542de 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\\Comments' => $baseDir . '/../lib/OpenMetrics/Comments.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..520b640625077 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\\Comments' => __DIR__ . '/..' . '/../lib/OpenMetrics/Comments.php',
'OCA\\Comments\\Search\\CommentsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/CommentsSearchProvider.php',
);
diff --git a/apps/comments/lib/OpenMetrics/Comments.php b/apps/comments/lib/OpenMetrics/Comments.php
new file mode 100644
index 0000000000000..38cfd6bb104e3
--- /dev/null
+++ b/apps/comments/lib/OpenMetrics/Comments.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..c6d60c7f5c114 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\SharesCount
+
diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php
index 48f197f9bf937..f29b6c341dc50 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\\SharesCount' => $baseDir . '/../lib/OpenMetrics/SharesCount.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..e43a9298a33e3 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\\SharesCount' => __DIR__ . '/..' . '/../lib/OpenMetrics/SharesCount.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/SharesCount.php b/apps/files_sharing/lib/OpenMetrics/SharesCount.php
new file mode 100644
index 0000000000000..86246db1ac0f8
--- /dev/null
+++ b/apps/files_sharing/lib/OpenMetrics/SharesCount.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;
+ }
+
+ while ($row = $result->fetch()) {
+ 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..0f70db5626992
--- /dev/null
+++ b/core/Controller/OpenMetricsController.php
@@ -0,0 +1,152 @@
+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 {
+ $exporter = $this->exporter;
+
+ foreach ($exporter() as $family) {
+ $output = '';
+ $name = $family->name();
+ if ($family->type() !== MetricTypes::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";
+
+ yield $output;
+ }
+
+ $elapsed = (string)(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']);
+ yield <<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..da27360bf626e 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
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 0b6dd29f2040b..4c41470a07d9f 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\\MetricTypes' => $baseDir . '/lib/public/OpenMetrics/MetricTypes.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',
@@ -1420,6 +1425,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',
@@ -1893,6 +1899,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\\Exporter' => $baseDir . '/lib/private/OpenMetrics/Exporter.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 9c34d2cd4debd..5abfbd4a325b4 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\\MetricTypes' => __DIR__ . '/../../..' . '/lib/public/OpenMetrics/MetricTypes.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',
@@ -1461,6 +1466,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',
@@ -1934,6 +1940,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\\Exporter' => __DIR__ . '/../../..' . '/lib/private/OpenMetrics/Exporter.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/Exporter.php b/lib/private/OpenMetrics/Exporter.php
new file mode 100644
index 0000000000000..aba387d1b5223
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporter.php
@@ -0,0 +1,97 @@
+skippedClasses = array_fill_keys($config->getSystemValue('openmetrics_skipped_classes', []), true);
+ }
+
+ public function __invoke(): 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 $appName) {
+ $appInfo = $this->appManager->getAppInfo($appName);
+ 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, $appName);
+ if ($exporter !== null) {
+ yield $exporter;
+ }
+ }
+ }
+ }
+
+ private function loadExporter(string $classname, string $appName = 'core'): ?IMetricFamily {
+ try {
+ return Server::get($classname);
+ } catch (\Exception $e) {
+ $this->logger->error(
+ 'Unable to build exporter {exporter}',
+ [
+ 'app' => $appName,
+ '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..50013ae970b30
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/ActiveSessions.php
@@ -0,0 +1,61 @@
+ $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..1b5267254d741
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/ActiveUsers.php
@@ -0,0 +1,61 @@
+ $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..ee038635fb331
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/AppsCount.php
@@ -0,0 +1,64 @@
+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..0adb53a92e360
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/AppsInfo.php
@@ -0,0 +1,58 @@
+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..298eedebba855
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/Cached.php
@@ -0,0 +1,60 @@
+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 = get_called_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..d2d9e3ceb2bef
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/FilesByType.php
@@ -0,0 +1,77 @@
+connection->getQueryBuilder();
+ $metrics = $qb->select('M.mimetype', $qb->func()->count('*', 'count'))
+ ->from('filecache', 'F')
+ ->join('F', 'mimetypes', 'M', $qb->expr()->eq('F.mimetype', 'M.id'))
+ ->groupBy('M.mimetype')
+ ->executeQuery();
+
+ if ($metrics->rowCount() === 0) {
+ yield new Metric(0);
+ return;
+ }
+ $now = time();
+ while ($count = $metrics->fetch()) {
+ yield new Metric($count['count'], ['mimetype' => $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..4c1e1507ba83f
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/InstanceInfo.php
@@ -0,0 +1,64 @@
+ $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..f74b8b391599d
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/Maintenance.php
@@ -0,0 +1,52 @@
+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..ac8abefa1a425
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/RunningJobs.php
@@ -0,0 +1,69 @@
+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;
+ }
+
+ while ($row = $result->fetch()) {
+ 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..937e23b242a8b
--- /dev/null
+++ b/lib/private/OpenMetrics/Exporters/UsersByBackend.php
@@ -0,0 +1,57 @@
+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..21b54496ecbcb
--- /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/MetricTypes.php b/lib/public/OpenMetrics/MetricTypes.php
new file mode 100644
index 0000000000000..54343aa65c9cc
--- /dev/null
+++ b/lib/public/OpenMetrics/MetricTypes.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->exporter = $this->createMock(Exporter::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->controller = new OpenMetricsController('core', $this->request, $this->config, $this->exporter, $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/ExporterTest.php b/tests/lib/OpenMetrics/ExporterTest.php
new file mode 100644
index 0000000000000..30d717dce8afb
--- /dev/null
+++ b/tests/lib/OpenMetrics/ExporterTest.php
@@ -0,0 +1,23 @@
+assertInstanceOf(Exporter::class, $exporter);
+ foreach ($exporter() 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..a87e42864808f
--- /dev/null
+++ b/tests/lib/OpenMetrics/Exporters/FilesByTypeTest.php
@@ -0,0 +1,27 @@
+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);
+ }
+ }
+}