[
  {
    "path": ".editorconfig",
    "content": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at https://editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nindent_size = 4\nindent_style = space\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.md]\ntrim_trailing_whitespace = false\n\n[*.yml]\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Path-based git attributes\n# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n\n# Ignore all test and documentation with \"export-ignore\".\n/.gitattributes     export-ignore\n/.gitignore         export-ignore\n/.travis.yml        export-ignore\n/phpunit.xml.dist   export-ignore\n/.scrutinizer.yml   export-ignore\n/tests              export-ignore\n/.editorconfig      export-ignore\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      fail-fast: true\n      matrix:\n        os: [ubuntu-latest]\n        php: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']\n        dependency-version: [prefer-stable]\n        redis-version: [6]\n\n    name: P${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n\n      - name: Cache dependencies\n        uses: actions/cache@v2\n        with:\n          path: ~/.composer/cache/files\n          key: dependencies-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}\n\n      - name: Setup PHP\n        uses: shivammathur/setup-php@v2\n        with:\n          php-version: ${{ matrix.php }}\n\n      - name: Install dependencies\n        run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest\n\n      - name: Execute tests\n        run: vendor/bin/phpunit\n"
  },
  {
    "path": ".gitignore",
    "content": "build\ncomposer.lock\ndocs\nvendor\ncoverage\n.phpunit.result.cache\n"
  },
  {
    "path": ".styleci.yml",
    "content": "preset: laravel\n\ndisabled:\n  - single_class_element_per_statement\n  - self_accessor\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nContributions are **welcome** and will be fully **credited**.\n\nPlease read and understand the contribution guide before creating an issue or pull request.\n\n## Etiquette\n\nThis project is open source, and as such, the maintainers give their free time to build and maintain the source code\nheld within. They make the code freely available in the hope that it will be of use to other developers. It would be\nextremely unfair for them to suffer abuse or anger for their hard work.\n\nPlease be considerate towards maintainers when raising issues or presenting pull requests. Let's show the\nworld that developers are civilized and selfless people.\n\nIt's the duty of the maintainer to ensure that all submissions to the project are of sufficient\nquality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.\n\n## Viability\n\nWhen requesting or submitting new features, first consider whether it might be useful to others. Open\nsource projects are used by many developers, who may have entirely different needs to your own. Think about\nwhether or not your feature is likely to be used by other users of the project.\n\n## Procedure\n\nBefore filing an issue:\n\n- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.\n- Check to make sure your feature suggestion isn't already present within the project.\n- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.\n- Check the pull requests tab to ensure that the feature isn't already in progress.\n\nBefore submitting a pull request:\n\n- Check the codebase to ensure that your feature doesn't already exist.\n- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.\n\n## Requirements\n\nIf the project maintainer has any additional requirements, you will find them listed here.\n\n- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).\n\n- **Add tests!** - Your patch won't be accepted if it doesn't have tests.\n\n- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.\n\n- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.\n\n- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.\n\n- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.\n\n**Happy coding**!\n"
  },
  {
    "path": "LICENSE.md",
    "content": "The MIT License (MIT)\n\nCopyright (c) LK-Development <info@lk-development.de>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Laravel Horizon Prometheus Exporter\n\n[![Latest Version on Packagist](https://img.shields.io/packagist/v/lkaemmerling/laravel-horizon-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/lkaemmerling/laravel-horizon-prometheus-exporter)\n[![Actions Status](https://github.com/lkaemmerling/laravel-horizon-prometheus-exporter/workflows/Tests/badge.svg)](https://github.com/lkaemmerling/laravel-horizon-prometheus-exporter/actions)\n[![Total Downloads](https://img.shields.io/packagist/dt/lkaemmerling/laravel-horizon-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/lkaemmerling/laravel-horizon-prometheus-exporter)\n\n\nThis package allows an easy way to expose the Laravel Horizon Metrics to Prometheus.\n\n## Prom... What?\n\nPrometheus is a scraping service which allows you to easily store and scrape information from your application, server or even from your router!\nPrometheus itself does not know about your application, so you need a exporter on your app. This small package is exactly this, an exporter which allows Prometheus to understand some information\nfrom your application. With Prometheus and a visualisation tool called `Grafana` you can build something like this beautiful Dashboard:\n\n![Laravel Horizon Prometheus Exporter Dashboard](https://pbs.twimg.com/media/EHdSoNGX4AEpbia?format=jpg&name=4096x4096)\n\n## Installation\n\nYou can install the package via composer:\n\n```bash\ncomposer require lkaemmerling/laravel-horizon-prometheus-exporter\n```\n\n## Configuration\n```bash\nphp artisan vendor:publish --provider=LKDevelopment\\\\HorizonPrometheusExporter\\\\HorizonPrometheusExporterServiceProvider\n```\nYou can configure this package by changing the values in `config/horizon-exporter.php`.\n\n## Custom Metrics\n\nYou can also use this package easily to expose custom metrics. You just need to implement the `LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter` interface and then add your implementation to your `config/horizon-exporter.php` like we do it for the Horizon exporters: https://github.com/LKaemmerling/laravel-horizon-prometheus-exporter/blob/master/config/config.php#L17\n\n## Dashboard\n\nYou can find a sample dashboard using this metrics on the [Grafana Marketplace](https://grafana.com/grafana/dashboards/11034).\n\n### Testing\n\n``` bash\ncomposer test\n```\n\n### Changelog\n\nPlease see [Releases](https://github.com/LKaemmerling/laravel-horizon-prometheus-exporter/releases) for more information on what has changed recently.\n\n## Contributing\n\nPlease see [CONTRIBUTING](CONTRIBUTING.md) for details.\n\n### Security\n\nIf you discover any security related issues, please email kontakt@lukas-kaemmerling.de instead of using the issue tracker.\n\n## Credits\n\n- [Lukas Kämmerling](https://github.com/LKaemmerling)\n- [All Contributors](../../contributors)\n\n## License\n\nThe MIT License (MIT). Please see [License File](LICENSE.md) for more information.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"lkaemmerling/laravel-horizon-prometheus-exporter\",\n    \"description\": \"A small package to gain and export long time information from Laravel & Horizon for Prometheus.\",\n    \"keywords\": [\n        \"laravel-horizon-prometheus-exporter\",\n        \"horizon\",\n        \"laravel\",\n        \"prometheus\",\n        \"exporter\"\n    ],\n    \"homepage\": \"https://github.com/lkaemmerling/laravel-horizon-prometheus-exporter\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"Lukas Kämmerling\",\n            \"email\": \"kontakt@lukas-kaemmerling.de\",\n            \"homepage\": \"https://lukas-kaemmerling.de\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^7.1|^8.0\",\n        \"promphp/prometheus_client_php\": \"^1.0.3|^2.0.0\",\n        \"illuminate/routing\": \"^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0\",\n        \"illuminate/support\": \"^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0\",\n        \"illuminate/config\": \"^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0|^13.0\",\n        \"laravel/horizon\": \"^4.0|^5.0\"\n    },\n    \"require-dev\": {\n        \"orchestra/testbench\": \"^v4.9|^5.3|^6.3|^7.0|^8.0|^9.0|^10.0|^11.0\",\n        \"phpunit/phpunit\": \"^8.2|^9.0|^10.5|^11.5.3|^12.5.12\",\n        \"symfony/var-dumper\": \"^4.3|^5.1|^7.2\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"LKDevelopment\\\\HorizonPrometheusExporter\\\\\": \"src\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"LKDevelopment\\\\HorizonPrometheusExporter\\\\Tests\\\\\": \"tests\"\n        }\n    },\n    \"scripts\": {\n        \"test\": \"vendor/bin/phpunit\",\n        \"test-coverage\": \"vendor/bin/phpunit --coverage-html coverage\"\n    },\n    \"config\": {\n        \"sort-packages\": true\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"LKDevelopment\\\\HorizonPrometheusExporter\\\\HorizonPrometheusExporterServiceProvider\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "config/.gitkeep",
    "content": ""
  },
  {
    "path": "config/config.php",
    "content": "<?php\nreturn [\n    \"namespace\" => 'app',\n    \"enabled\" => env('HORIZON_PROMETHEUS_EXPORTER_ENABLED', true),\n\n    /**\n     * You can change the default endpoint to something other than metrics.\n     * Keep in mind that the change needs to be reflected in your Prometheus configuration as well.\n     */\n    \"url\" => 'metrics',\n\n    /**\n     * You can enable or disable or even create own exporters by simply implementing the LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter Contract.\n     * If you want to disable oder enable a Exporter just comment the specific line out.\n     * If you want to add your own Exporter just add the Class Name to this array\n     */\n    \"exporters\" => [\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\CurrentMasterSupervisors::class,\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\JobsPerMinute::class,\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\CurrentWorkload::class,\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\CurrentProcessesPerQueue::class,\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\FailedJobsPerHour::class,\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\HorizonStatus::class,\n        \\LKDevelopment\\HorizonPrometheusExporter\\Exporter\\RecentJobs::class\n    ],\n\n    /**\n     * IP Whitelisting, you may don't want to expose your metrics on the internet so you can add the IP addresses of your Prometheus Server here.\n     */\n    \"ip_whitelist\" => [\n        // Keep empty to allow all IP addresses\n    ],\n\n    /**\n     * You can change the Middleware which is used for the IP whitelisting.  You can add your own, like a token based authentication.\n     */\n    \"middleware\" => \\LKDevelopment\\HorizonPrometheusExporter\\Http\\Middleware\\IPWhitelistingMiddleware::class,\n\n    /**\n     * Allow storage to be wiped after a render of data in metrics controller\n     */\n    \"wipe_storage_after_render\" => false,\n];\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit bootstrap=\"vendor/autoload.php\"\n         backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         colors=\"true\"\n         verbose=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\">\n    <testsuites>\n        <testsuite name=\"Spatie Test Suite\">\n            <directory>tests</directory>\n        </testsuite>\n    </testsuites>\n    <filter>\n        <whitelist>\n            <directory suffix=\".php\">src/</directory>\n        </whitelist>\n    </filter>\n    <logging>\n        <log type=\"tap\" target=\"build/report.tap\"/>\n        <log type=\"junit\" target=\"build/report.junit.xml\"/>\n        <log type=\"coverage-html\" target=\"build/coverage\"/>\n        <log type=\"coverage-text\" target=\"build/coverage.txt\"/>\n        <log type=\"coverage-clover\" target=\"build/logs/clover.xml\"/>\n    </logging>\n</phpunit>\n"
  },
  {
    "path": "routes/api.php",
    "content": "<?php\nRoute::get(config('horizon-exporter.url'), \\LKDevelopment\\HorizonPrometheusExporter\\Http\\Controller\\HorizonPrometheusExporterController::class.'@metrics');\n\n"
  },
  {
    "path": "src/Contracts/Exporter.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Contracts;\n\nuse Prometheus\\CollectorRegistry;\n\n/**\n * Interface Exporter\n * Describes an metric exporter that generate the metrics\n * @package LKDevelopment\\HorizonPrometheusExporter\\Contracts\n */\ninterface Exporter\n{\n    /**\n     * The metrics method is used to register/describe your metrics to the exporter.\n     * @param CollectorRegistry $collectorRegistry\n     * @void\n     */\n    public function metrics(CollectorRegistry $collectorRegistry): void;\n\n    /**\n     * The collect method is called from the Exporter when he collects the data.\n     * You don't need to call it manually. You should perform the whole data\n     * collection within this method.\n     * @void\n     */\n    public function collect(): void;\n}\n"
  },
  {
    "path": "src/Exporter/CurrentMasterSupervisors.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\n\nuse Laravel\\Horizon\\Contracts\\MasterSupervisorRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass CurrentMasterSupervisors implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n\t\t$this->gauge = $collectorRegistry->getOrRegisterGauge(\n\t\t\tconfig('horizon-exporter.namespace'),\n\t\t\t'horizon_current_mastersupervisors',\n\t\t\t'Number of mastersupervisors'\n\t\t);\n    }\n\n    public function collect(): void\n    {\n\t\t$number = count(app(MasterSupervisorRepository::class)->all());\n\n\t\t$this->gauge->set($number);\n    }\n}\n"
  },
  {
    "path": "src/Exporter/CurrentProcessesPerQueue.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\nuse Laravel\\Horizon\\Contracts\\WorkloadRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass CurrentProcessesPerQueue implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->gauge = $collectorRegistry->getOrRegisterGauge(\n            config('horizon-exporter.namespace'),\n            'horizon_current_processes',\n            'Current processes of all queues',\n            ['queue']\n        );\n    }\n\n    public function collect(): void\n    {\n        $workloadRepository = app(WorkloadRepository::class);\n        $workloads = collect($workloadRepository->get())->sortBy('name')->values();\n\n        $workloads->each(function ($workload) {\n            $this->gauge->set($workload['processes'], [$workload['name']]);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Exporter/CurrentWorkload.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\n\nuse Laravel\\Horizon\\Contracts\\WorkloadRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass CurrentWorkload implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->gauge = $collectorRegistry->getOrRegisterGauge(\n            config('horizon-exporter.namespace'),\n            'horizon_current_workload',\n            'Current workload of all queues',\n            ['queue']\n        );\n    }\n\n    public function collect(): void\n    {\n        $workloadRepository = app(WorkloadRepository::class);\n        $workloads = collect($workloadRepository->get())->sortBy('name')->values();\n\n        $workloads->each(function ($workload) {\n            if (isset($workload['split_queues']) && $workload['split_queues']) {\n                $workload['split_queues']->each(function ($queue) {\n                    $this->gauge->set($queue['length'], [$queue['name']]);\n                });\n\n                return;\n            }\n\n            $this->gauge->set($workload['length'], [$workload['name']]);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Exporter/FailedJobsPerHour.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\n\nuse Laravel\\Horizon\\Contracts\\JobRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass FailedJobsPerHour implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->gauge = $collectorRegistry->getOrRegisterGauge(\n            config('horizon-exporter.namespace'),\n            'horizon_failed_jobs',\n            'The number of recently failed jobs'\n        );\n    }\n\n    public function collect(): void\n    {\n        $this->gauge->set(app(JobRepository::class)->countRecentlyFailed());\n    }\n}\n"
  },
  {
    "path": "src/Exporter/HorizonStatus.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\n\nuse Laravel\\Horizon\\Contracts\\MasterSupervisorRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass HorizonStatus implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->gauge = $collectorRegistry->getOrRegisterGauge(\n            config('horizon-exporter.namespace'),\n            'horizon_status',\n            'The status of Horizon, -1 = inactive, 0 = paused, 1 = running'\n        );\n    }\n\n    public function collect(): void\n    {\n        $status = -1;\n        if ($masters = app(MasterSupervisorRepository::class)->all()) {\n            $status = collect($masters)->contains(function ($master) {\n                return $master->status === 'paused';\n            }) ? 0 : 1;\n        }\n        $this->gauge->set($status);\n    }\n}\n"
  },
  {
    "path": "src/Exporter/JobsPerMinute.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\n\nuse Laravel\\Horizon\\Contracts\\MetricsRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass JobsPerMinute implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->gauge = $collectorRegistry->getOrRegisterGauge(\n            config('horizon-exporter.namespace'),\n            'horizon_jobs_per_minute',\n            'The number of jobs per minute'\n        );\n    }\n\n    public function collect(): void\n    {\n        $this->gauge->set(app(MetricsRepository::class)->jobsProcessedPerMinute());\n    }\n}\n"
  },
  {
    "path": "src/Exporter/RecentJobs.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Exporter;\n\n\nuse Laravel\\Horizon\\Contracts\\JobRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Gauge;\n\nclass RecentJobs implements Exporter\n{\n    protected Gauge $gauge;\n\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->gauge = $collectorRegistry->getOrRegisterGauge(\n            config('horizon-exporter.namespace'),\n            'horizon_recent_jobs',\n            'The number of recent jobs'\n        );\n    }\n\n    public function collect(): void\n    {\n        $this->gauge->set(app(JobRepository::class)->countRecent());\n    }\n}\n"
  },
  {
    "path": "src/HorizonPrometheusExporterServiceProvider.php",
    "content": "<?php\n\nnamespace LKDevelopment\\HorizonPrometheusExporter;\n\nuse Illuminate\\Support\\Facades\\Route;\nuse Illuminate\\Support\\ServiceProvider;\n\nclass HorizonPrometheusExporterServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap the application services.\n     */\n    public function boot(): void\n    {\n        if ($this->app->runningInConsole()) {\n            $this->publishes([\n                __DIR__ . '/../config/config.php' => config_path('horizon-exporter.php'),\n            ], 'config');\n        }\n        $this->app->booted(function () {\n            $this->routes();\n        });\n    }\n\n    /**\n     * Register the application services.\n     */\n    public function register(): void\n    {\n        $this->mergeConfigFrom(__DIR__ . '/../config/config.php', 'horizon-exporter');\n    }\n\n    protected function routes(): void\n    {\n        if ($this->app->routesAreCached()) {\n            return;\n        }\n\n        if (config('horizon-exporter.enabled')) {\n            Route::middleware(config('horizon-exporter.middleware'))->group(\n                __DIR__ . '/../routes/api.php'\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Http/Controller/HorizonPrometheusExporterController.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Http\\Controller;\n\n\nuse Illuminate\\Http\\Response;\nuse Illuminate\\Routing\\Controller;\nuse LKDevelopment\\HorizonPrometheusExporter\\Repository\\ExporterRepository;\nuse Prometheus\\RenderTextFormat;\n\nclass HorizonPrometheusExporterController extends Controller\n{\n    public function metrics()\n    {\n        ExporterRepository::load();\n        $renderer = new RenderTextFormat();\n        $result = $renderer->render(ExporterRepository::getRegistry()->getMetricFamilySamples());\n\n        if(config('horizon-exporter.wipe_storage_after_render', false)) {\n            ExporterRepository::getRegistry()->wipeStorage();\n        }\n\n        return new Response($result, Response::HTTP_OK, [\"Content-Type\" => RenderTextFormat::MIME_TYPE]);\n    }\n}\n"
  },
  {
    "path": "src/Http/Middleware/IPWhitelistingMiddleware.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Http\\Middleware;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse Symfony\\Component\\HttpFoundation\\IpUtils;\n\nclass IPWhitelistingMiddleware\n{\n    public function handle(Request $request, \\Closure $next)\n    {\n        if (!empty(config('horizon-exporter.ip_whitelist'))) {\n            $clientIp = $request->ip();\n            if (IpUtils::checkIp($clientIp, config('horizon-exporter.ip_whitelist'))) {\n                return $next($request);\n            } else {\n                abort(403);\n            }\n        } else {\n            return $next($request);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Repository/ExporterRepository.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Repository;\n\n\nuse LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter;\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Storage\\InMemory;\n\n/**\n * Class ExporterRepository\n * @package LKDevelopment\\HorizonPrometheusExporter\\Repository\n */\nclass ExporterRepository\n{\n    /**\n     * @var CollectorRegistry\n     */\n    protected static $registry;\n\n    /**\n     * @param array $exporters\n     */\n    public static function load(array $exporters = []): void\n    {\n        $_exporters = empty($exporters) ? config('horizon-exporter.exporters') : $exporters;\n\n        if (self::getRegistry() === null) {\n            self::setRegistry(new CollectorRegistry(new InMemory()));\n        }\n        foreach ($_exporters as $exporter) {\n            $_exporter = new $exporter();\n            /**\n             * @var Exporter $_exporter\n             */\n            $_exporter->metrics(self::$registry);\n            $_exporter->collect();\n        }\n    }\n\n    /**\n     * @param CollectorRegistry $collectorRegistry\n     */\n    public static function setRegistry(CollectorRegistry $collectorRegistry): void\n    {\n        self::$registry = $collectorRegistry;\n    }\n\n    /**\n     * @return CollectorRegistry\n     */\n    public static function getRegistry()\n    {\n        return self::$registry;\n    }\n}\n"
  },
  {
    "path": "tests/Http/Controller/HorizonPrometheusExporterControllerTest.php",
    "content": "<?php\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Tests\\Http\\Controller;\n\nuse LKDevelopment\\HorizonPrometheusExporter\\Http\\Controller\\HorizonPrometheusExporterController;\nuse LKDevelopment\\HorizonPrometheusExporter\\Repository\\ExporterRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Tests\\Util\\NoopExporter;\nuse LKDevelopment\\HorizonPrometheusExporter\\Tests\\TestCase;\n\nclass HorizonPrometheusExporterControllerTest extends TestCase\n{\n\n    public function testMetrics()\n    {\n        $ctrl = new HorizonPrometheusExporterController();\n\n        $resp = $ctrl->metrics();\n        $expected = <<<EOF\n# HELP app_noop_metric noop metric\n# TYPE app_noop_metric counter\napp_noop_metric{op=\"noop\"} 1\nEOF;\n        self::assertStringContainsString($expected, $resp);\n\n    }\n}\n"
  },
  {
    "path": "tests/Http/Middleware/IPWhitelistingMiddlewareTest.php",
    "content": "<?php\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Tests\\Http\\Middleware;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Http\\Response;\nuse LKDevelopment\\HorizonPrometheusExporter\\Http\\Middleware\\IPWhitelistingMiddleware;\nuse LKDevelopment\\HorizonPrometheusExporter\\Tests\\TestCase;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpException;\n\nclass IPWhitelistingMiddlewareTest extends TestCase\n{\n    /**\n     * @dataProvider testCases\n     */\n    public function testHandle($requestingIP, $expectedStatusCode)\n    {\n        $middleware = new IPWhitelistingMiddleware();\n        $statusCode = null;\n        try {\n            $statusCode = $middleware->handle(new Request([], [], [], [], [], ['REMOTE_ADDR' => $requestingIP]), \\Closure::fromCallable(function ($next) {\n                return new Response();\n            }))->getStatusCode();\n\n        } catch (HttpException $httpException) {\n            $statusCode = $httpException->getStatusCode();\n        }\n        self::assertEquals($expectedStatusCode, $statusCode);\n    }\n\n    public function testCases()\n    {\n        return [\n            [\n                \"127.0.0.1\", // Requesting IP\n                Response::HTTP_OK // Expected Status Code\n            ],\n            [\n                \"127.0.0.2\",\n                Response::HTTP_FORBIDDEN\n            ],\n            [\n                \"10.0.0.1\",\n                Response::HTTP_OK\n            ],\n            [\n                \"10.0.1.1\",\n                Response::HTTP_FORBIDDEN\n            ]\n        ];\n    }\n}\n"
  },
  {
    "path": "tests/Repository/ExporterRepositoryTest.php",
    "content": "<?php\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Tests\\Repository;\n\nuse LKDevelopment\\HorizonPrometheusExporter\\Repository\\ExporterRepository;\nuse LKDevelopment\\HorizonPrometheusExporter\\Tests\\Util\\NoopExporter;\nuse LKDevelopment\\HorizonPrometheusExporter\\Tests\\TestCase;\n\nclass ExporterRepositoryTest extends TestCase\n{\n\n    public function testLoad()\n    {\n        ExporterRepository::load([NoopExporter::class]);\n        self::assertNotNull(ExporterRepository::getRegistry());\n\n        $counter = ExporterRepository::getRegistry()->getCounter(\"app\", \"noop_metric\");\n        self::assertNotNull($counter);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Tests;\n\nuse LKDevelopment\\HorizonPrometheusExporter\\Tests\\Util\\NoopExporter;\n\nclass TestCase extends \\Orchestra\\Testbench\\TestCase\n{\n    protected function getEnvironmentSetUp($app)\n    {\n        $app['config']->set('horizon-exporter.exporters', [NoopExporter::class]);\n        $app['config']->set('horizon-exporter.ip_whitelist', [\"127.0.0.1\", \"10.0.0.0/24\"]);\n    }\n}\n"
  },
  {
    "path": "tests/Util/NoopExporter.php",
    "content": "<?php\n\n\nnamespace LKDevelopment\\HorizonPrometheusExporter\\Tests\\Util;\n\n\nuse Prometheus\\CollectorRegistry;\nuse Prometheus\\Counter;\n\n/**\n * Class NoopExporter\n * @package LKDevelopment\\HorizonPrometheusExporter\\Tests\\Util\n */\nclass NoopExporter implements \\LKDevelopment\\HorizonPrometheusExporter\\Contracts\\Exporter\n{\n    /**\n     * @var Counter\n     */\n    protected Counter $counter;\n    /**\n     * @inheritDoc\n     */\n    public function metrics(CollectorRegistry $collectorRegistry): void\n    {\n        $this->counter = $collectorRegistry->getOrRegisterCounter(\n            \"app\",\n            'noop_metric',\n            'noop metric',\n            [\"op\"]\n        );\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function collect(): void\n    {\n        $this->counter->inc([\"op\" => \"noop\"]);\n    }\n}\n"
  }
]