Skip to content

Commit 62513df

Browse files
authored
Merge pull request #57165 from nextcloud/feat/openmetrics
2 parents c09168e + 71fa593 commit 62513df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1708
-9
lines changed

apps/comments/appinfo/info.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
</providers>
3939
</activity>
4040

41+
<openmetrics>
42+
<exporter>OCA\Comments\OpenMetrics\CommentsCountMetric</exporter>
43+
</openmetrics>
44+
4145
<collaboration>
4246
<plugins>
4347
<plugin type="autocomplete-sort">OCA\Comments\Collaboration\CommentersSorter</plugin>

apps/comments/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@
2222
'OCA\\Comments\\MaxAutoCompleteResultsInitialState' => $baseDir . '/../lib/MaxAutoCompleteResultsInitialState.php',
2323
'OCA\\Comments\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php',
2424
'OCA\\Comments\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
25+
'OCA\\Comments\\OpenMetrics\\CommentsCountMetric' => $baseDir . '/../lib/OpenMetrics/CommentsCountMetric.php',
2526
'OCA\\Comments\\Search\\CommentsSearchProvider' => $baseDir . '/../lib/Search/CommentsSearchProvider.php',
2627
);

apps/comments/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class ComposerStaticInitComments
3737
'OCA\\Comments\\MaxAutoCompleteResultsInitialState' => __DIR__ . '/..' . '/../lib/MaxAutoCompleteResultsInitialState.php',
3838
'OCA\\Comments\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php',
3939
'OCA\\Comments\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
40+
'OCA\\Comments\\OpenMetrics\\CommentsCountMetric' => __DIR__ . '/..' . '/../lib/OpenMetrics/CommentsCountMetric.php',
4041
'OCA\\Comments\\Search\\CommentsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/CommentsSearchProvider.php',
4142
);
4243

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
namespace OCA\Comments\OpenMetrics;
8+
9+
use Generator;
10+
use OC\DB\Connection;
11+
use OCP\OpenMetrics\IMetricFamily;
12+
use OCP\OpenMetrics\Metric;
13+
use OCP\OpenMetrics\MetricType;
14+
use Override;
15+
16+
class CommentsCountMetric implements IMetricFamily {
17+
public function __construct(
18+
private Connection $connection,
19+
) {
20+
}
21+
22+
#[Override]
23+
public function name(): string {
24+
return 'comments';
25+
}
26+
27+
#[Override]
28+
public function type(): MetricType {
29+
return MetricType::gauge;
30+
}
31+
32+
#[Override]
33+
public function unit(): string {
34+
return 'comments';
35+
}
36+
37+
#[Override]
38+
public function help(): string {
39+
return 'Number of comments';
40+
}
41+
42+
#[Override]
43+
public function metrics(): Generator {
44+
$qb = $this->connection->getQueryBuilder();
45+
$result = $qb->select($qb->func()->count())
46+
->from('comments')
47+
->where($qb->expr()->eq('verb', $qb->expr()->literal('comment')))
48+
->executeQuery();
49+
50+
yield new Metric($result->fetchOne(), [], time());
51+
}
52+
}

apps/files_sharing/appinfo/info.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,8 @@ Turning the feature off removes shared files and folders on the server for all s
8787
<public>
8888
<files>public.php</files>
8989
</public>
90+
91+
<openmetrics>
92+
<exporter>OCA\Files_Sharing\OpenMetrics\SharesCountMetric</exporter>
93+
</openmetrics>
9094
</info>

apps/files_sharing/composer/composer/autoload_classmap.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
'OCA\\Files_Sharing\\MountProvider' => $baseDir . '/../lib/MountProvider.php',
8989
'OCA\\Files_Sharing\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php',
9090
'OCA\\Files_Sharing\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
91+
'OCA\\Files_Sharing\\OpenMetrics\\SharesCountMetric' => $baseDir . '/../lib/OpenMetrics/SharesCountMetric.php',
9192
'OCA\\Files_Sharing\\OrphanHelper' => $baseDir . '/../lib/OrphanHelper.php',
9293
'OCA\\Files_Sharing\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
9394
'OCA\\Files_Sharing\\Scanner' => $baseDir . '/../lib/Scanner.php',

apps/files_sharing/composer/composer/autoload_static.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class ComposerStaticInitFiles_Sharing
103103
'OCA\\Files_Sharing\\MountProvider' => __DIR__ . '/..' . '/../lib/MountProvider.php',
104104
'OCA\\Files_Sharing\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php',
105105
'OCA\\Files_Sharing\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
106+
'OCA\\Files_Sharing\\OpenMetrics\\SharesCountMetric' => __DIR__ . '/..' . '/../lib/OpenMetrics/SharesCountMetric.php',
106107
'OCA\\Files_Sharing\\OrphanHelper' => __DIR__ . '/..' . '/../lib/OrphanHelper.php',
107108
'OCA\\Files_Sharing\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
108109
'OCA\\Files_Sharing\\Scanner' => __DIR__ . '/..' . '/../lib/Scanner.php',
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Files_Sharing\OpenMetrics;
11+
12+
use Generator;
13+
use OCP\DB\QueryBuilder\IQueryBuilder;
14+
use OCP\IDBConnection;
15+
use OCP\OpenMetrics\IMetricFamily;
16+
use OCP\OpenMetrics\Metric;
17+
use OCP\OpenMetrics\MetricType;
18+
use OCP\Share\IShare;
19+
use Override;
20+
21+
/**
22+
* Count shares by type
23+
* @since 33.0.0
24+
*/
25+
class SharesCountMetric implements IMetricFamily {
26+
public function __construct(
27+
private IDBConnection $connection,
28+
) {
29+
}
30+
31+
#[Override]
32+
public function name(): string {
33+
return 'shares';
34+
}
35+
36+
#[Override]
37+
public function type(): MetricType {
38+
return MetricType::gauge;
39+
}
40+
41+
#[Override]
42+
public function unit(): string {
43+
return 'shares';
44+
}
45+
46+
#[Override]
47+
public function help(): string {
48+
return 'Number of shares by type';
49+
}
50+
51+
#[Override]
52+
public function metrics(): Generator {
53+
$types = [
54+
IShare::TYPE_USER => 'user',
55+
IShare::TYPE_GROUP => 'group',
56+
IShare::TYPE_LINK => 'link',
57+
IShare::TYPE_EMAIL => 'email',
58+
];
59+
$qb = $this->connection->getQueryBuilder();
60+
$result = $qb->select($qb->func()->count('*', 'count'), 'share_type')
61+
->from('share')
62+
->where($qb->expr()->in('share_type', $qb->createNamedParameter(array_keys($types), IQueryBuilder::PARAM_INT_ARRAY)))
63+
->groupBy('share_type')
64+
->executeQuery();
65+
66+
if ($result->rowCount() === 0) {
67+
yield new Metric(0);
68+
return;
69+
}
70+
71+
foreach ($result->iterateAssociative() as $row) {
72+
yield new Metric($row['count'], ['type' => $types[$row['share_type']]]);
73+
}
74+
}
75+
}

config/config.sample.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2892,4 +2892,29 @@
28922892
* Defaults to `\OC::$SERVERROOT . '/resources/config/ca-bundle.crt'`.
28932893
*/
28942894
'default_certificates_bundle_path' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt',
2895+
2896+
/**
2897+
* OpenMetrics skipped exporters
2898+
* Allows to skip some exporters in the OpenMetrics endpoint ``/metrics``.
2899+
*
2900+
* Default to ``[]`` (empty array)
2901+
*/
2902+
'openmetrics_skipped_classes' => [
2903+
'OC\OpenMetrics\Exporters\FilesByType',
2904+
'OCA\Files_Sharing\OpenMetrics\SharesCount',
2905+
],
2906+
2907+
/**
2908+
* OpenMetrics allowed client IP addresses
2909+
* Restricts the IP addresses able to make requests on the ``/metrics`` endpoint.
2910+
*
2911+
* Keep this list as restrictive as possible as metrics can consume a lot of resources.
2912+
*
2913+
* Default to ``[127.0.0.0/16', '::1/128]`` (allow loopback interface only)
2914+
*/
2915+
'openmetrics_allowed_clients' => [
2916+
'192.168.0.0/16',
2917+
'fe80::/10',
2918+
'10.0.0.1',
2919+
],
28952920
];
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OC\Core\Controller;
9+
10+
use OC\OpenMetrics\ExporterManager;
11+
use OC\Security\Ip\Address;
12+
use OC\Security\Ip\Range;
13+
use OCP\AppFramework\Controller;
14+
use OCP\AppFramework\Http;
15+
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
16+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
17+
use OCP\AppFramework\Http\Attribute\PublicPage;
18+
use OCP\IConfig;
19+
use OCP\IRequest;
20+
use OCP\OpenMetrics\IMetricFamily;
21+
use OCP\OpenMetrics\Metric;
22+
use OCP\OpenMetrics\MetricType;
23+
use OCP\OpenMetrics\MetricValue;
24+
use Psr\Log\LoggerInterface;
25+
26+
/**
27+
* OpenMetrics controller
28+
*
29+
* Gather and display metrics
30+
*
31+
* @package OC\Core\Controller
32+
*/
33+
class OpenMetricsController extends Controller {
34+
public function __construct(
35+
string $appName,
36+
IRequest $request,
37+
private IConfig $config,
38+
private ExporterManager $exporterManager,
39+
private LoggerInterface $logger,
40+
) {
41+
parent::__construct($appName, $request);
42+
}
43+
44+
#[NoCSRFRequired]
45+
#[PublicPage]
46+
#[FrontpageRoute(verb: 'GET', url: '/metrics')]
47+
public function export(): Http\Response {
48+
if (!$this->isRemoteAddressAllowed()) {
49+
return new Http\Response(Http::STATUS_FORBIDDEN);
50+
}
51+
52+
return new Http\StreamTraversableResponse(
53+
$this->generate(),
54+
Http::STATUS_OK,
55+
[
56+
'Content-Type' => 'application/openmetrics-text; version=1.0.0; charset=utf-8',
57+
]
58+
);
59+
}
60+
61+
private function isRemoteAddressAllowed(): bool {
62+
$clientAddress = new Address($this->request->getRemoteAddress());
63+
$allowedRanges = $this->config->getSystemValue('openmetrics_allowed_clients', ['127.0.0.0/16', '::1/128']);
64+
if (!is_array($allowedRanges)) {
65+
$this->logger->warning('Invalid configuration for "openmetrics_allowed_clients"');
66+
return false;
67+
}
68+
69+
foreach ($allowedRanges as $range) {
70+
$range = new Range($range);
71+
if ($range->contains($clientAddress)) {
72+
return true;
73+
}
74+
}
75+
76+
return false;
77+
}
78+
79+
private function generate(): \Generator {
80+
foreach ($this->exporterManager->export() as $family) {
81+
yield $this->formatFamily($family);
82+
}
83+
84+
$elapsed = (string)(microtime(true) - $_SERVER['REQUEST_TIME_FLOAT']);
85+
yield <<<SUMMARY
86+
# TYPE nextcloud_exporter_duration gauge
87+
# UNIT nextcloud_exporter_duration seconds
88+
# HELP nextcloud_exporter_duration Exporter run time
89+
nextcloud_exporter_duration $elapsed
90+
91+
# EOF
92+
93+
SUMMARY;
94+
}
95+
96+
private function formatFamily(IMetricFamily $family): string {
97+
$output = '';
98+
$name = $family->name();
99+
if ($family->type() !== MetricType::unknown) {
100+
$output = '# TYPE nextcloud_' . $name . ' ' . $family->type()->name . "\n";
101+
}
102+
if ($family->unit() !== '') {
103+
$output .= '# UNIT nextcloud_' . $name . ' ' . $family->unit() . "\n";
104+
}
105+
if ($family->help() !== '') {
106+
$output .= '# HELP nextcloud_' . $name . ' ' . $family->help() . "\n";
107+
}
108+
foreach ($family->metrics() as $metric) {
109+
$output .= 'nextcloud_' . $name . $this->formatLabels($metric) . ' ' . $this->formatValue($metric);
110+
if ($metric->timestamp !== null) {
111+
$output .= ' ' . $this->formatTimestamp($metric);
112+
}
113+
$output .= "\n";
114+
}
115+
$output .= "\n";
116+
117+
return $output;
118+
}
119+
120+
private function formatLabels(Metric $metric): string {
121+
if (empty($metric->labels)) {
122+
return '';
123+
}
124+
125+
$labels = [];
126+
foreach ($metric->labels as $label => $value) {
127+
$labels[] .= $label . '=' . $this->escapeString((string)$value);
128+
}
129+
130+
return '{' . implode(',', $labels) . '}';
131+
}
132+
133+
private function escapeString(string $string): string {
134+
return json_encode(
135+
$string,
136+
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
137+
1
138+
);
139+
}
140+
141+
private function formatValue(Metric $metric): string {
142+
if (is_bool($metric->value)) {
143+
return $metric->value ? '1' : '0';
144+
}
145+
if ($metric->value instanceof MetricValue) {
146+
return $metric->value->value;
147+
}
148+
149+
return (string)$metric->value;
150+
}
151+
152+
private function formatTimestamp(Metric $metric): string {
153+
return (string)$metric->timestamp;
154+
}
155+
}

0 commit comments

Comments
 (0)