feat(openmetrics): introduce OpenMetrics exporter

Expose a `/metrics` endpoint with some basic metrics

Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
This commit is contained in:
Benjamin Gaussorgues 2025-08-18 16:24:23 +02:00
parent c09168e911
commit c57c4843e8
No known key found for this signature in database
37 changed files with 1551 additions and 10 deletions

View file

@ -2892,4 +2892,29 @@ $CONFIG = [
* 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',
],
];

View file

@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\Core\Controller;
use OC\OpenMetrics\ExporterManager;
use OC\Security\Ip\Address;
use OC\Security\Ip\Range;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\IConfig;
use OCP\IRequest;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use OCP\OpenMetrics\MetricValue;
use Psr\Log\LoggerInterface;
/**
* OpenMetrics controller
*
* Gather and display metrics
*
* @package OC\Core\Controller
*/
class OpenMetricsController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IConfig $config,
private ExporterManager $exporterManager,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
#[NoCSRFRequired]
#[PublicPage]
#[FrontpageRoute(verb: 'GET', url: '/metrics')]
public function export(): Http\Response {
if (!$this->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 <<<SUMMARY
# TYPE nextcloud_exporter_duration gauge
# UNIT nextcloud_exporter_duration seconds
# HELP nextcloud_exporter_duration Exporter run time
nextcloud_exporter_duration $elapsed
# EOF
SUMMARY;
}
private function formatFamily(IMetricFamily $family): string {
$output = '';
$name = $family->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;
}
}

View file

@ -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.

View file

@ -122,6 +122,7 @@ return array(
'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 @@ return array(
'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 @@ return array(
'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 @@ return array(
'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',

View file

@ -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',

View file

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics;
use Generator;
use OC\Log\PsrLoggerAdapter;
use OCP\App\IAppManager;
use OCP\IConfig;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
class ExporterManager {
private array $skippedClasses;
private const XML_ENTRY = 'openmetrics';
public function __construct(
private IAppManager $appManager,
private PsrLoggerAdapter $logger,
IConfig $config,
) {
// Use values as keys for faster lookups
$this->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;
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
class ActiveSessions implements IMetricFamily {
public function __construct(
private IDBConnection $connection,
) {
}
public function name(): string {
return 'active_sessions';
}
public function type(): MetricType {
return MetricType::gauge;
}
public function unit(): string {
return 'sessions';
}
public function help(): string {
return 'Number of active sessions';
}
public function metrics(): Generator {
$now = time();
$timeFrames = [
'Last 5 minutes' => $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]);
}
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
class ActiveUsers implements IMetricFamily {
public function __construct(
private IDBConnection $connection,
) {
}
public function name(): string {
return 'active_users';
}
public function type(): MetricType {
return MetricType::gauge;
}
public function unit(): string {
return 'users';
}
public function help(): string {
return 'Number of active users';
}
public function metrics(): Generator {
$now = time();
$timeFrames = [
'Last 5 minutes' => $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]);
}
}
}

View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\App\IAppManager;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use Override;
/**
* Export statistics about apps
*/
class AppsCount implements IMetricFamily {
public function __construct(
private IAppManager $appManager,
) {
}
#[Override]
public function name(): string {
return 'apps_count';
}
#[Override]
public function type(): MetricType {
return MetricType::gauge;
}
#[Override]
public function unit(): string {
return 'applications';
}
#[Override]
public function help(): string {
return 'Number of apps in Nextcloud';
}
#[Override]
public function metrics(): Generator {
$installedAppsCount = count($this->appManager->getAppInstalledVersions(false));
$enabledAppsCount = count($this->appManager->getEnabledApps());
$disabledAppsCount = $installedAppsCount - $enabledAppsCount;
yield new Metric(
$disabledAppsCount,
['status' => 'disabled'],
);
yield new Metric(
$enabledAppsCount,
['status' => 'enabled'],
);
}
}

View file

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\App\IAppManager;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use Override;
/**
* Export information about enabled applications
*/
class AppsInfo implements IMetricFamily {
public function __construct(
private IAppManager $appManager,
) {
}
#[Override]
public function name(): string {
return 'apps_info';
}
#[Override]
public function type(): MetricType {
return MetricType::info;
}
#[Override]
public function unit(): string {
return '';
}
#[Override]
public function help(): string {
return 'Enabled applications in Nextcloud';
}
#[Override]
public function metrics(): Generator {
yield new Metric(
1,
$this->appManager->getAppInstalledVersions(true),
time()
);
}
}

View file

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\OpenMetrics\IMetricFamily;
use Override;
/**
* Cached metrics
*/
abstract class Cached implements IMetricFamily {
private readonly ICache $cache;
public function __construct(
ICacheFactory $cacheFactory,
) {
$this->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());
}
}

View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\Files\IMimeTypeLoader;
use OCP\ICacheFactory;
use OCP\IDBConnection;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use Override;
/**
* Export files count
*
* Cached exporter, refreshed every 30 minutes
*/
class FilesByType extends Cached {
public function __construct(
ICacheFactory $cacheFactory,
private IDBConnection $connection,
private IMimeTypeLoader $mimetypeLoader,
) {
parent::__construct($cacheFactory);
}
#[Override]
public function name(): string {
return 'files';
}
#[Override]
public function type(): MetricType {
return MetricType::gauge;
}
#[Override]
public function unit(): string {
return 'files';
}
#[Override]
public function help(): string {
return 'Number of files by type';
}
#[Override]
public function getTTL(): int {
return 30 * 60;
}
#[Override]
public function gatherMetrics(): Generator {
$qb = $this->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,
);
}
}
}

View file

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OC\SystemConfig;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use OCP\ServerVersion;
use Override;
/**
* Export some basic information about current instance
*/
class InstanceInfo implements IMetricFamily {
public function __construct(
private SystemConfig $systemConfig,
private ServerVersion $serverVersion,
) {
}
#[Override]
public function name(): string {
return 'instance_info';
}
#[Override]
public function type(): MetricType {
return MetricType::info;
}
#[Override]
public function unit(): string {
return '';
}
#[Override]
public function help(): string {
return 'Basic information about Nextcloud';
}
#[Override]
public function metrics(): Generator {
yield new Metric(
1,
[
'full version' => $this->serverVersion->getHumanVersion(),
'major version' => (string)$this->serverVersion->getVersion()[0],
'build' => $this->serverVersion->getBuild(),
'installed' => $this->systemConfig->getValue('installed', false) ? '1' : '0',
],
time()
);
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OC\SystemConfig;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use OCP\Server;
use Override;
/**
* Export maintenance state
*/
class Maintenance implements IMetricFamily {
public function name(): string {
return 'maintenance';
}
#[Override]
public function type(): MetricType {
return MetricType::info;
}
#[Override]
public function unit(): string {
return '';
}
#[Override]
public function help(): string {
return 'Maintenance status of Nextcloud';
}
#[Override]
public function metrics(): Generator {
$systemConfig = Server::get(SystemConfig::class);
yield new Metric(
(bool)$systemConfig->getValue('maintenance', false)
);
}
}

View file

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use Override;
/**
* Export the number of running jobs by type
*/
class RunningJobs implements IMetricFamily {
public function __construct(
private IDBConnection $connection,
) {
}
#[Override]
public function name(): string {
return 'jobs_running';
}
#[Override]
public function type(): MetricType {
return MetricType::gauge;
}
#[Override]
public function unit(): string {
return 'jobs';
}
#[Override]
public function help(): string {
return 'Number of running jobs';
}
#[Override]
public function metrics(): Generator {
$qb = $this->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']]);
}
}
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OC\OpenMetrics\Exporters;
use Generator;
use OCP\IUserManager;
use OCP\OpenMetrics\IMetricFamily;
use OCP\OpenMetrics\Metric;
use OCP\OpenMetrics\MetricType;
use Override;
/**
* Count users of each backend which supports it (mapped users only)
*/
class UsersByBackend implements IMetricFamily {
public function __construct(
private IUserManager $userManager,
) {
}
#[Override]
public function name(): string {
return 'users';
}
#[Override]
public function type(): MetricType {
return MetricType::gauge;
}
#[Override]
public function unit(): string {
return 'users';
}
#[Override]
public function help(): string {
return 'Number of users by backend';
}
#[Override]
public function metrics(): Generator {
$userCounts = $this->userManager->countUsers(true);
foreach ($userCounts as $backend => $count) {
yield new Metric($count, ['backend' => $backend]);
}
}
}

View file

@ -443,22 +443,23 @@ class Manager extends PublicEmitter implements IUserManager {
/**
* 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<string, int> 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 @@ class Manager extends PublicEmitter implements IUserManager {
}
}
}
return $userCountStatistics;
}

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\AppFramework\Http;
use OCP\AppFramework\Http;
use Override;
use Traversable;
/**
* Class StreamResponse
*
* @since 33.0.0
* @template S of Http::STATUS_*
* @template H of array<string, mixed>
* @template-extends Response<Http::STATUS_*, array<string, mixed>>
*/
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();
}
}
}

View file

@ -162,8 +162,9 @@ interface IUserManager {
*
* @return array<string, int> 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

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\OpenMetrics;
use Generator;
use OCP\AppFramework\Attribute\Implementable;
/**
* @since 33.0.0
*/
#[Implementable(since: '33.0.0')]
interface IMetricFamily {
/**
* Family name (will be prefixed by nextcloud_)
*
* @since 33.0.0
*/
public function name(): string;
/**
* Family metric type
*
* @since 33.0.0
*/
public function type(): MetricType;
/**
* Family unit (can be empty string)
* @since 33.0.0
*/
public function unit(): string;
/**
* Family help text (can be empty string)
* @since 33.0.0
*/
public function help(): string;
/**
* List of metrics
*
* @return Generator<Metric>
* @since 33.0.0
*/
public function metrics(): Generator;
}

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\OpenMetrics;
/**
* @since 33.0.0
*/
final readonly class Metric {
public function __construct(
public int|float|bool|MetricValue $value = false,
/** @var string[] */
public array $labels = [],
public int|float|null $timestamp = null,
) {
}
public function label(string $name): ?string {
return $this->labels[$name] ?? null;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\OpenMetrics;
/**
* Metrics types
*
* @since 33.0.0
*/
enum MetricType {
case counter;
case gauge;
case histogram;
case gaugehistogram;
case stateset;
case info;
case summary;
case unknown;
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCP\OpenMetrics;
/**
* Special values for metrics
* @since 33.0.0
*/
enum MetricValue: string {
case NOT_A_NUMBER = 'NaN';
case POSITIVE_INFINITY = '+Inf';
case NEGATIVE_INFINITY = '-Inf';
}

View file

@ -16970,7 +16970,8 @@
"schema": {
"type": "object",
"required": [
"fileId"
"fileId",
"expirationTime"
],
"properties": {
"fileId": {

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Tests\Core\Controller;
use OC\Core\Controller\OpenMetricsController;
use OC\OpenMetrics\ExporterManager;
use OCP\AppFramework\Http\IOutput;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StreamTraversableResponse;
use OCP\IConfig;
use OCP\IRequest;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class OpenMetricsControllerTest extends TestCase {
private IRequest&MockObject $request;
private IConfig&MockObject $config;
private ExporterManager&MockObject $exporterManager;
private LoggerInterface&MockObject $logger;
private OpenMetricsController $controller;
protected function setUp(): void {
parent::setUp();
$this->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 = <<<EXPECTED
# TYPE nextcloud_exporter_duration gauge
# UNIT nextcloud_exporter_duration seconds
# HELP nextcloud_exporter_duration Exporter run time
nextcloud_exporter_duration %f
# EOF
EXPECTED;
$response->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());
}
}

View file

@ -9,6 +9,7 @@ namespace Test;
use OCP\App\IAppManager;
use OCP\AppFramework\App;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
/**
@ -130,5 +131,14 @@ class InfoXmlTest extends TestCase {
$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);
}
}
}
}

View file

@ -0,0 +1,23 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics;
use OC\OpenMetrics\ExporterManager;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
use Test\TestCase;
class ExporterManagerTest extends TestCase {
public function testExport(): void {
$exporter = Server::get(ExporterManager::class);
$this->assertInstanceOf(ExporterManager::class, $exporter);
foreach ($exporter->export() as $metric) {
$this->assertInstanceOf(IMetricFamily::class, $metric);
};
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\ActiveSessions;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
use PHPUnit\Framework\Attributes\Group;
#[Group('DB')]
class ActiveSessionsTest extends ExporterTestCase {
protected function getExporter():IMetricFamily {
return new ActiveSessions(Server::get(IDBConnection::class));
}
public function testMetricsLabel(): void {
$this->assertLabelsAre([
['time' => 'Last 5 minutes'],
['time' => 'Last 15 minutes'],
['time' => 'Last hour'],
['time' => 'Last day'],
]);
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\ActiveUsers;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
use PHPUnit\Framework\Attributes\Group;
#[Group('DB')]
class ActiveUsersTest extends ExporterTestCase {
protected function getExporter():IMetricFamily {
return new ActiveUsers(Server::get(IDBConnection::class));
}
public function testMetricsLabel(): void {
$this->assertLabelsAre([
['time' => 'Last 5 minutes'],
['time' => 'Last 15 minutes'],
['time' => 'Last hour'],
['time' => 'Last day'],
]);
}
}

View file

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\AppsCount;
use OCP\App\IAppManager;
use OCP\OpenMetrics\IMetricFamily;
class AppsCountTest extends ExporterTestCase {
private IAppManager $appManager;
protected function getExporter():IMetricFamily {
$this->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);
}
}
}

View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\AppsInfo;
use OCP\App\IAppManager;
use OCP\OpenMetrics\IMetricFamily;
class AppsInfoTest extends ExporterTestCase {
private IAppManager $appManager;
private array $appList = [
'appA' => '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);
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OCP\OpenMetrics\IMetricFamily;
use Test\TestCase;
abstract class ExporterTestCase extends TestCase {
protected IMetricFamily $exporter;
/** @var IMetric[] */
protected array $metrics;
abstract protected function getExporter(): IMetricFamily;
protected function setUp(): void {
parent::setUp();
$this->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);
}
}

View file

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\FilesByType;
use OCP\Files\IMimeTypeLoader;
use OCP\ICacheFactory;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
use PHPUnit\Framework\Attributes\Group;
#[Group('DB')]
class FilesByTypeTest extends ExporterTestCase {
protected function getExporter():IMetricFamily {
return new FilesByType(
Server::get(ICacheFactory::class),
Server::get(IDBConnection::class),
Server::get(IMimeTypeLoader::class),
);
}
}

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\InstanceInfo;
use OC\SystemConfig;
use OCP\OpenMetrics\IMetricFamily;
use OCP\ServerVersion;
use PHPUnit\Framework\MockObject\MockObject;
class InstanceInfoTest extends ExporterTestCase {
private SystemConfig&MockObject $systemConfig;
private ServerVersion&MockObject $serverVersion;
protected function getExporter():IMetricFamily {
$this->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);
}
}

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\Maintenance;
use OCP\OpenMetrics\IMetricFamily;
class MaintenanceTest extends ExporterTestCase {
protected function getExporter():IMetricFamily {
return new Maintenance();
}
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\RunningJobs;
use OCP\IDBConnection;
use OCP\OpenMetrics\IMetricFamily;
use OCP\Server;
use PHPUnit\Framework\Attributes\Group;
#[Group('DB')]
class RunningJobsTest extends ExporterTestCase {
protected function getExporter():IMetricFamily {
return new RunningJobs(Server::get(IDBConnection::class));
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace Test\OpenMetrics\Exporters;
use OC\OpenMetrics\Exporters\UsersByBackend;
use OCP\IUserManager;
use OCP\OpenMetrics\IMetricFamily;
use PHPUnit\Framework\MockObject\MockObject;
class UsersByBackendTest extends ExporterTestCase {
private IUserManager&MockObject $userManager;
private array $backendList = [
'backend A' => 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);
}
}
}