Skip to content

Commit

Permalink
Merge pull request #441 from systemli/Add-Command-for-exporting-Metrics
Browse files Browse the repository at this point in the history
Add Command for exporting Metrics
  • Loading branch information
0x46616c6b authored Mar 29, 2023
2 parents 2bd720e + 0c300b2 commit 953be62
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 3.2.0 (UNRELEASED)

* Add Command to export metrics to Prometheus

# 3.1.0 (2022.10.26)

* Add Two-factor authentication support
Expand Down
103 changes: 103 additions & 0 deletions src/Command/MetricsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace App\Command;

use App\Entity\Alias;
use App\Entity\Domain;
use App\Entity\OpenPgpKey;
use App\Entity\User;
use App\Entity\Voucher;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Class MetricsCommand.
*
* This command exposes metrics for Prometheus. It is intended to be used as a
* cronjob. It can be used together with the Prometheus Node Exporter (textfile
* collector).
*
* Example for Cron:
* * * * * * php /path/to/bin/console app:metrics | sponge /path/to/metrics/userli.prom
*/
class MetricsCommand extends Command
{
private EntityManagerInterface $manager;

/**
* MetricsCommand constructor.
*/
public function __construct(EntityManagerInterface $manager)
{
parent::__construct();
$this->manager = $manager;
}

/**
* {@inheritdoc}
*/
protected function configure(): void
{
# TODO: Option for exposing metrics in different formats (prometheus, openmetrics, etc.)
$this
->setName('app:metrics')
->setDescription('Global Metrics for Userli');
}

/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$usersTotal = $this->manager->getRepository(User::class)->countUsers();
$output->writeln('# HELP userli_users_total Total number of users');
$output->writeln('# TYPE userli_users_total gauge');
$output->writeln('userli_users_total ' . $usersTotal);

$deletedUsersTotal = $this->manager->getRepository(User::class)->countDeletedUsers();
$output->writeln('# HELP userli_users_deleted_total Total number of deleted users');
$output->writeln('# TYPE userli_users_deleted_total gauge');
$output->writeln('userli_users_deleted_total ' . $deletedUsersTotal);

$usersRecoveryTokenTotal = $this->manager->getRepository(User::class)->countUsersWithRecoveryToken();
$output->writeln('# HELP userli_users_recovery_token_total Total number of users with recovery token');
$output->writeln('# TYPE userli_users_recovery_token_total gauge');
$output->writeln('userli_users_recovery_token_total ' . $usersRecoveryTokenTotal);

$usersMailCryptTotal = $this->manager->getRepository(User::class)->countUsersWithMailCrypt();
$output->writeln('# HELP userli_users_mailcrypt_total Total number of users with enabled mailcrypt');
$output->writeln('# TYPE userli_users_mailcrypt_total gauge');
$output->writeln('userli_users_mailcrypt_total ' . $usersMailCryptTotal);

$usersTwofactorTotal = $this->manager->getRepository(User::class)->countUsersWithTwofactor();
$output->writeln('# HELP userli_users_twofactor_total Total number of users with enabled two factor authentication');
$output->writeln('# TYPE userli_users_twofactor_total gauge');
$output->writeln('userli_users_twofactor_total ' . $usersTwofactorTotal);

$redeemedVouchersTotal = $this->manager->getRepository(Voucher::class)->countRedeemedVouchers();
$unredeemedVouchersTotal = $this->manager->getRepository(Voucher::class)->countUnredeemedVouchers();
$output->writeln('# HELP userli_vouchers_total Total number of vouchers');
$output->writeln('# TYPE userli_vouchers_total gauge');
$output->writeln('userli_vouchers_total{type="unredeemed"} ' . $unredeemedVouchersTotal);
$output->writeln('userli_vouchers_total{type="redeemed"} ' . $redeemedVouchersTotal);

$domainsTotal = $this->manager->getRepository(Domain::class)->count([]);
$output->writeln('# HELP userli_domains_total Total number of domains');
$output->writeln('# TYPE userli_domains_total gauge');
$output->writeln('userli_domains_total ' . $domainsTotal);

$aliasesTotal = $this->manager->getRepository(Alias::class)->count(['deleted' => false]);
$output->writeln('# HELP userli_aliases_total Total number of aliases');
$output->writeln('# TYPE userli_aliases_total gauge');
$output->writeln('userli_aliases_total ' . $aliasesTotal);

$openPgpKeysTotal = $this->manager->getRepository(OpenPgpKey::class)->countKeys();
$output->writeln('# HELP userli_openpgpkeys_total Total number of OpenPGP keys');
$output->writeln('# TYPE userli_openpgpkeys_total gauge');
$output->writeln('userli_openpgpkeys_total ' . $openPgpKeysTotal);

return 0;
}
}
2 changes: 1 addition & 1 deletion src/DataFixtures/LoadUserData.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private function loadStaticUsers(ObjectManager $manager): void
*/
private function loadRandomUsers(ObjectManager $manager): void
{
$domainRepository = $manager->getRepository(Domain::classn);
$domainRepository = $manager->getRepository(Domain::class);

for ($i = 0; $i < 500; ++$i) {
$email = sprintf('%[email protected]', uniqid('', true));
Expand Down
21 changes: 21 additions & 0 deletions src/Repository/VoucherRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
class VoucherRepository extends AbstractRepository
{
/**
* Finds a voucher by its code.
*
* @param $code
*
* @return Voucher|object|null
Expand All @@ -21,12 +23,29 @@ public function findByCode($code)
return $this->findOneBy(['code' => $code]);
}

/**
* Returns the number of redeemed vouchers.
*
* @return int
*/
public function countRedeemedVouchers(): int
{
return $this->matching(Criteria::create()->where(Criteria::expr()->neq('redeemedTime', null)))->count();
}

/**
* Returns the number of unredeemed vouchers.
*
* @return int
*/
public function countUnredeemedVouchers(): int
{
return $this->matching(Criteria::create()->where(Criteria::expr()->eq('redeemedTime', null)))->count();
}

/**
* Finds all vouchers for a given user.
*
* @return array|Voucher[]
*/
public function findByUser(User $user): array
Expand All @@ -35,6 +54,8 @@ public function findByUser(User $user): array
}

/**
* Get all redeemed vouchers that are older than 3 months.
*
* @return Voucher[]|array
*/
public function getOldVouchers(): array
Expand Down
83 changes: 83 additions & 0 deletions tests/Command/MetricsCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace App\Tests\Command;

use App\Command\MetricsCommand;
use App\Entity\Alias;
use App\Entity\Domain;
use App\Entity\OpenPgpKey;
use App\Entity\User;
use App\Entity\Voucher;
use App\Repository\AliasRepository;
use App\Repository\DomainRepository;
use App\Repository\OpenPgpKeyRepository;
use App\Repository\UserRepository;
use App\Repository\VoucherRepository;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Tester\CommandTester;

class MetricsCommandTest extends TestCase
{
public function testExecute(): void
{
$userRepository = $this->getMockBuilder(UserRepository::class)
->disableOriginalConstructor()
->getMock();
$userRepository->method('countUsers')->willReturn(10);
$userRepository->method('countDeletedUsers')->willReturn(3);
$userRepository->method('countUsersWithRecoveryToken')->willReturn(5);
$userRepository->method('countUsersWithMailCrypt')->willReturn(7);
$userRepository->method('countUsersWithTwofactor')->willReturn(9);

$openPgpKeyRepository = $this->getMockBuilder(OpenPgpKeyRepository::class)
->disableOriginalConstructor()
->getMock();
$openPgpKeyRepository->method('countKeys')->willReturn(2);

$aliasRepository = $this->getMockBuilder(AliasRepository::class)
->disableOriginalConstructor()
->getMock();
$aliasRepository->method('count')->willReturn(4);

$domainRepository = $this->getMockBuilder(DomainRepository::class)
->disableOriginalConstructor()
->getMock();
$domainRepository->method('count')->willReturn(6);

$voucherRepository = $this->getMockBuilder(VoucherRepository::class)
->disableOriginalConstructor()
->getMock();
$voucherRepository->method('countRedeemedVouchers')->willReturn(1);
$voucherRepository->method('countUnredeemedVouchers')->willReturn(7);

$manager = $this->getMockBuilder(EntityManagerInterface::class)
->disableOriginalConstructor()
->getMock();
$manager->method('getRepository')->willReturnMap([
[User::class, $userRepository],
[OpenPgpKey::class, $openPgpKeyRepository],
[Alias::class, $aliasRepository],
[Domain::class, $domainRepository],
[Voucher::class, $voucherRepository],
]);

$command = new MetricsCommand($manager);

$commandTester = new CommandTester($command);
$commandTester->execute([]);

$output = $commandTester->getDisplay();

self::assertStringContainsString("userli_users_total 10", $output);
self::assertStringContainsString("userli_users_deleted_total 3", $output);
self::assertStringContainsString("userli_users_recovery_token_total 5", $output);
self::assertStringContainsString("userli_users_mailcrypt_total 7", $output);
self::assertStringContainsString("userli_users_twofactor_total 9", $output);
self::assertStringContainsString("userli_vouchers_total{type=\"unredeemed\"} 7", $output);
self::assertStringContainsString("userli_vouchers_total{type=\"redeemed\"} 1", $output);
self::assertStringContainsString("userli_domains_total 6", $output);
self::assertStringContainsString("userli_aliases_total 4", $output);
self::assertStringContainsString("userli_openpgpkeys_total 2", $output);
}
}

0 comments on commit 953be62

Please sign in to comment.