From 0c300b26f215ffbbd18623845aba0c6a42b8d3d5 Mon Sep 17 00:00:00 2001 From: louis Date: Wed, 29 Mar 2023 11:55:17 +0200 Subject: [PATCH] Add Command for exporting Metrics --- CHANGELOG.md | 4 ++ src/Command/MetricsCommand.php | 103 +++++++++++++++++++++++++++ src/DataFixtures/LoadUserData.php | 2 +- src/Repository/VoucherRepository.php | 21 ++++++ tests/Command/MetricsCommandTest.php | 83 +++++++++++++++++++++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 src/Command/MetricsCommand.php create mode 100644 tests/Command/MetricsCommandTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cafe06a..a21da414 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Command/MetricsCommand.php b/src/Command/MetricsCommand.php new file mode 100644 index 00000000..5581057d --- /dev/null +++ b/src/Command/MetricsCommand.php @@ -0,0 +1,103 @@ +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; + } +} diff --git a/src/DataFixtures/LoadUserData.php b/src/DataFixtures/LoadUserData.php index 7259619e..e8d850f9 100644 --- a/src/DataFixtures/LoadUserData.php +++ b/src/DataFixtures/LoadUserData.php @@ -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('%s@example.org', uniqid('', true)); diff --git a/src/Repository/VoucherRepository.php b/src/Repository/VoucherRepository.php index 1429910d..c38e4231 100644 --- a/src/Repository/VoucherRepository.php +++ b/src/Repository/VoucherRepository.php @@ -12,6 +12,8 @@ class VoucherRepository extends AbstractRepository { /** + * Finds a voucher by its code. + * * @param $code * * @return Voucher|object|null @@ -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 @@ -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 diff --git a/tests/Command/MetricsCommandTest.php b/tests/Command/MetricsCommandTest.php new file mode 100644 index 00000000..f9e74a1d --- /dev/null +++ b/tests/Command/MetricsCommandTest.php @@ -0,0 +1,83 @@ +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); + } +}