diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bcc67af --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[*.yml] +indent_size = 2 + +[*.yaml] +indent_size = 2 diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..84b95cd --- /dev/null +++ b/.env.sample @@ -0,0 +1,16 @@ +APP_ENV=production + +# Debug mode set to TRUE disables view caching and enables higher verbosity. +DEBUG=true +VERBOSITY_LEVEL=verbose + +# Available levels: DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY +MONOLOG_DEFAULT_LEVEL=DEBUG + +QUEUE_DRIVER=roadrunner +QUEUE_PIPELINE=memory + +CACHE_STORAGE=local + +# Packagist repository names to follow (comma separated) +PACKAGIST_REPOSITORIES= diff --git a/.github/workflows/docker-dev-image.yml b/.github/workflows/docker-dev-image.yml new file mode 100644 index 0000000..794d370 --- /dev/null +++ b/.github/workflows/docker-dev-image.yml @@ -0,0 +1,36 @@ +name: Docker Dev Image CI + +on: + push: + branches: + - master + +jobs: + build-release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v3 + with: + context: ./ + file: ./docker/Dockerfile + push: true + build-args: + APP_VERSION=dev + tags: + ghcr.io/metrixio/docker:dev diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..067d12e --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,43 @@ +name: Docker Image CI + +on: + release: + types: + - created + +jobs: + build-release: + if: "!github.event.release.prerelease" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: 'Get Previous tag' + id: previoustag + uses: "WyriHaximus/github-action-get-previous-tag@v1" + with: + fallback: v0.1 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ secrets.GHCR_LOGIN }} + password: ${{ secrets.GHCR_PASSWORD }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v2 + + - name: Build and push + id: docker_build + uses: docker/build-push-action@v3 + with: + context: ./ + file: ./docker/Dockerfile + push: true + build-args: + APP_VERSION=${{ steps.previoustag.outputs.tag }} + tags: + ghcr.io/metrixio/docker:latest, ghcr.io/metrixio/docker:${{ steps.previoustag.outputs.tag }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6c86483 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea +vendor +runtime +rr* +spiral* +.env +.phpunit.result.cache +.deptrac.cache +composer.lock diff --git a/.rr.yaml b/.rr.yaml new file mode 100644 index 0000000..49062c2 --- /dev/null +++ b/.rr.yaml @@ -0,0 +1,29 @@ +version: '2.7' + +rpc: + listen: tcp://127.0.0.1:6001 + +logs: + level: info + +server: + command: "php app.php" + relay: pipes + +kv: + local: + driver: memory + config: {} + +jobs: + pool: + num_workers: 2 + +service: + collector: + command: "php app.php collect:start" + remain_after_exit: true + restart_sec: 1 + +metrics: + address: 0.0.0.0:2112 diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..aa7f13d --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,18 @@ +risky: false +preset: psr12 +enabled: + # Risky Fixers + # - declare_strict_types + # - void_return + - ordered_class_elements + - linebreak_after_opening_tag + - single_quote + - no_blank_lines_after_phpdoc + - unary_operator_spaces + - no_useless_else + - no_useless_return + - trailing_comma_in_multiline_array +finder: + exclude: + - "tests" + - "public" diff --git a/LICENSE b/LICENSE index 5049f95..be7d3b9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2022 metrix.io +Copyright (c) 2020 Spiral Scout Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..c23e61a --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Packagist metrics collector + +![packagist](https://user-images.githubusercontent.com/773481/209584409-3275bfa7-f131-44de-b4c1-341d4b0cd3d3.png) + +This tool lets you easily gather data about downloads from Packagist. + +It works with Prometheus and Grafana to collect data from Packagist, store it in Prometheus, and create visualizations +with Grafana. You can use Grafana to customize the data you collect and create dashboards that fit your needs. + +We hope you find it helpful! + +## Dashboard + +![image](https://user-images.githubusercontent.com/773481/209584323-9f673696-f235-4737-b7ea-719da8c3ada3.png) + +## Usage + +Check out the documentation in the [dashboard](https://github.com/metrixio/dashboard) repository. That should give you +all the details you need to get going. + +```dotenv +# Packagist repository names to follow (comma separated) +PACKAGIST_REPOSITORIES=... +``` + +### Docker + +```yaml +version: "3.7" + +services: + docker-metrics: + image: ghcr.io/metrixio/packagist:latest + environment: + PACKAGIST_REPOSITORIES: ... + restart: on-failure + + prometheus: + image: prom/prometheus + volumes: + - ./runtime/prometheus:/prometheus + restart: always + + grafana: + image: grafana/grafana + depends_on: + - prometheus + ports: + - 3000:3000 + volumes: + - ./runtime/grafana:/var/lib/grafana + restart: always +``` + +### Local server + +```bash +composer create-project metrixio/packagist +``` + +Define the repositories you want to track in `.env` file + +```dotenv +PACKAGIST_REPOSITORIES=spiral/framework,... +``` + +Once the project is installed and configured you can start application server: + +```bash +./rr serve +``` + +Metrics will be available on http://127.0.0.1:2112. + +> **Note**: +> To fix unable to open metrics page, change metrics address in RoadRunner config file to `127.0.0.1:2112`. + +----- + +The package is built with some of the best tools out there for PHP. It's powered +by [Spiral Framework](https://github.com/spiral/framework/), which makes it super fast and efficient, and it +uses [RoadRunner](https://github.com/roadrunner-server/roadrunner) as the server, which is a really great tool for +collecting metrics data for Prometheus. diff --git a/app.php b/app.php new file mode 100644 index 0000000..e8a04e6 --- /dev/null +++ b/app.php @@ -0,0 +1,30 @@ + __DIR__], +)->run(); + +if ($app === null) { + exit(255); +} + +$code = (int)$app->serve(); +exit($code); diff --git a/app/config/cache.php b/app/config/cache.php new file mode 100644 index 0000000..bfdf7e7 --- /dev/null +++ b/app/config/cache.php @@ -0,0 +1,19 @@ + env('CACHE_STORAGE', 'local'), + 'aliases' => [ + 'repositories' => 'local', + ], + 'storages' => [ + 'local' => [ + 'type' => 'roadrunner', + 'driver' => 'local', + ], + ], +]; diff --git a/app/config/monolog.php b/app/config/monolog.php new file mode 100644 index 0000000..a4fdf4c --- /dev/null +++ b/app/config/monolog.php @@ -0,0 +1,16 @@ + 'roadrunner', + 'globalLevel' => Logger::toMonologLevel(env('MONOLOG_DEFAULT_LEVEL', Logger::DEBUG)), + 'handlers' => [ + 'roadrunner' => [ + \Spiral\RoadRunnerBridge\Logger\Handler::class, + ] + ], +]; diff --git a/app/config/packagist.php b/app/config/packagist.php new file mode 100644 index 0000000..12f5c41 --- /dev/null +++ b/app/config/packagist.php @@ -0,0 +1,7 @@ + [], +]; diff --git a/app/config/queue.php b/app/config/queue.php new file mode 100644 index 0000000..74565c1 --- /dev/null +++ b/app/config/queue.php @@ -0,0 +1,34 @@ + env('QUEUE_DRIVER', 'roadrunner'), + 'aliases' => [], + 'connections' => [ + 'roadrunner' => [ + 'driver' => 'roadrunner', + 'default' => env('QUEUE_PIPELINE', 'memory'), + 'pipelines' => [ + 'memory' => [ + 'connector' => new MemoryCreateInfo('local'), + 'consume' => true, + ], + ], + ], + ], + + 'registry' => [ + 'handlers' => [], + ], + + 'interceptors' => [ + 'push' => [], + 'consume' => [ + \Spiral\Queue\Interceptor\Consume\ErrorHandlerInterceptor::class, + ], + ], +]; diff --git a/app/src/Api/Cli/CollectMetrics.php b/app/src/Api/Cli/CollectMetrics.php new file mode 100644 index 0000000..6a35033 --- /dev/null +++ b/app/src/Api/Cli/CollectMetrics.php @@ -0,0 +1,59 @@ +getInterval(); + + while (true) { + $metrics->declare(new PackagistCollectors()); + + foreach ($registry->getRepositories() as $repository) { + $logger->debug('Collecting metrics', ['repository' => $repository]); + + $queue->push( + PackagistDataCollector::class, + ['repository' => $repository], + (new Options())->withHeader('attempts', '5') + ); + } + + \sleep($interval); + } + + return self::SUCCESS; + } + + public function getInterval(): int + { + return \max( + (int)$this->option('interval'), + 10 + ); + } +} diff --git a/app/src/Application/Bootloader/ExceptionHandlerBootloader.php b/app/src/Application/Bootloader/ExceptionHandlerBootloader.php new file mode 100644 index 0000000..54800c2 --- /dev/null +++ b/app/src/Application/Bootloader/ExceptionHandlerBootloader.php @@ -0,0 +1,32 @@ + EnvSuppressErrors::class, + RendererInterface::class => PlainRenderer::class, + ]; + + public function boot( + ExceptionHandlerInterface $handler, + LoggerReporter $logger, + FileReporter $files, + ): void { + $handler->addReporter($logger); + $handler->addReporter($files); + } +} diff --git a/app/src/Application/Bootloader/PackagistBootloader.php b/app/src/Application/Bootloader/PackagistBootloader.php new file mode 100644 index 0000000..5bafbd8 --- /dev/null +++ b/app/src/Application/Bootloader/PackagistBootloader.php @@ -0,0 +1,44 @@ + [self::class, 'initPackagistClient'], + PackagistRepositoryRegistry::class => [self::class, 'initRegistry'], + ]; + + private function initRegistry( + PackagistConfigRepository $configRepository, + PackagistEvnRepository $envRepository, + ): PackagistRepositoryRegistry { + $repositories = \array_merge( + $configRepository->all(), + $envRepository->all(), + ); + + return new PackagistRepositoryRegistry( + \array_unique($repositories) + ); + } + + private function initPackagistClient(): ClientInterface + { + return new Client( + new NativeHttpClient([ + 'base_uri' => 'https://packagist.org', + ]) + ); + } +} diff --git a/app/src/Application/Job/PackagistDataCollector.php b/app/src/Application/Job/PackagistDataCollector.php new file mode 100644 index 0000000..c2688a2 --- /dev/null +++ b/app/src/Application/Job/PackagistDataCollector.php @@ -0,0 +1,53 @@ +warning('Attempt to fetch [%s] docker data failed', $repo); + return; + } + + try { + $package = $client->getPackageInformation($repo); + + $metrics->setDownloads((float)$package->downloads->total, $package->name); + $metrics->setDownloadsMonthly((float)$package->downloads->monthly, $package->name); + $metrics->setDownloadsDaily((float)$package->downloads->daily, $package->name); + + } catch (\Throwable $e) { + $reporter->report($e); + + throw new RetryException( + reason: $e->getMessage(), + options: (new Options())->withDelay(5)->withHeader('attempts', (string)($attempts - 1)) + ); + } + } +} diff --git a/app/src/Application/Kernel.php b/app/src/Application/Kernel.php new file mode 100644 index 0000000..2913578 --- /dev/null +++ b/app/src/Application/Kernel.php @@ -0,0 +1,56 @@ +rpc = $this->rpc->withServicePrefix('metrics'); + } + + public function declare(CollectorsInterface $collectors): self + { + foreach ($collectors as $name => $collector) { + try { + $this->rpc->call('Unregister', $name); + } catch (ServiceException $e) { + } + + $this->metrics->declare($name, $collector); + } + + return $this; + } +} diff --git a/app/src/Application/Metrics/CollectorsInterface.php b/app/src/Application/Metrics/CollectorsInterface.php new file mode 100644 index 0000000..583cf6e --- /dev/null +++ b/app/src/Application/Metrics/CollectorsInterface.php @@ -0,0 +1,15 @@ + + */ + public function getIterator(): \Traversable; +} diff --git a/app/src/Application/Metrics/PackagistCollectors.php b/app/src/Application/Metrics/PackagistCollectors.php new file mode 100644 index 0000000..7476267 --- /dev/null +++ b/app/src/Application/Metrics/PackagistCollectors.php @@ -0,0 +1,29 @@ + Collector::gauge() + ->withHelp('Package downloads statistics.') + ->withLabels('package'); + + yield self::DOWNLOADS_MONTHLY => Collector::gauge() + ->withHelp('Package monthly downloads statistics.') + ->withLabels('package'); + + yield self::DOWNLOADS_DAILY => Collector::gauge() + ->withHelp('Package daily downloads statistics.') + ->withLabels('package'); + } +} diff --git a/app/src/Application/Metrics/PackagistMetrics.php b/app/src/Application/Metrics/PackagistMetrics.php new file mode 100644 index 0000000..444fc92 --- /dev/null +++ b/app/src/Application/Metrics/PackagistMetrics.php @@ -0,0 +1,30 @@ +metrics->set(PackagistCollectors::DOWNLOADS, $count, [$repo]); + } + + public function setDownloadsMonthly(float $count, mixed $repo): void + { + $this->metrics->set(PackagistCollectors::DOWNLOADS_MONTHLY, $count, [$repo]); + } + + public function setDownloadsDaily(float $count, mixed $repo): void + { + $this->metrics->set(PackagistCollectors::DOWNLOADS_DAILY, $count, [$repo]); + } +} diff --git a/app/src/Application/PackagistRepositoryRegistry.php b/app/src/Application/PackagistRepositoryRegistry.php new file mode 100644 index 0000000..792e149 --- /dev/null +++ b/app/src/Application/PackagistRepositoryRegistry.php @@ -0,0 +1,46 @@ +repositories[] = $repository; + } + + /** + * @param TRepository $repository + */ + public function remove(string $repository): void + { + $this->repositories = \array_filter( + $this->repositories, + static fn(string $item): bool => $item !== $repository + ); + } + + /** + * @return TRepository[] + */ + public function getRepositories(): array + { + return $this->repositories; + } +} diff --git a/app/src/Application/Repository/PackagistConfigRepository.php b/app/src/Application/Repository/PackagistConfigRepository.php new file mode 100644 index 0000000..268628d --- /dev/null +++ b/app/src/Application/Repository/PackagistConfigRepository.php @@ -0,0 +1,20 @@ +config->getRepositories(); + } +} diff --git a/app/src/Application/Repository/PackagistEvnRepository.php b/app/src/Application/Repository/PackagistEvnRepository.php new file mode 100644 index 0000000..5e56c91 --- /dev/null +++ b/app/src/Application/Repository/PackagistEvnRepository.php @@ -0,0 +1,26 @@ +env->get('PACKAGIST_REPOSITORIES'); + + if ($data === null) { + return []; + } + + return \array_filter(\explode(',', (string)$data)); + } +} diff --git a/app/src/Application/Repository/PackagistRepositoryInterface.php b/app/src/Application/Repository/PackagistRepositoryInterface.php new file mode 100644 index 0000000..cc87949 --- /dev/null +++ b/app/src/Application/Repository/PackagistRepositoryInterface.php @@ -0,0 +1,10 @@ +httpClient->request('GET', "/packages/{$package}.json"); + + if ($response->getStatusCode() !== 200) { + throw new \RuntimeException(\sprintf('Package [%s] something went wrong', $package)); + } + + $data = \json_decode($response->getContent(), true); + + return new Package( + name: $data['package']['name'], + description: $data['package']['description'], + repository: $data['package']['repository'], + downloads: new Downloads( + total: $data['package']['downloads']['total'], + monthly: $data['package']['downloads']['monthly'], + daily: $data['package']['downloads']['daily'], + ), + github: new Github( + stars: $data['package']['github_stars'], + watchers: $data['package']['github_watchers'], + forks: $data['package']['github_forks'], + openIssues: $data['package']['github_open_issues'], + ), + ); + } +} diff --git a/app/src/Infrastructure/Packagist/ClientInterface.php b/app/src/Infrastructure/Packagist/ClientInterface.php new file mode 100644 index 0000000..7570916 --- /dev/null +++ b/app/src/Infrastructure/Packagist/ClientInterface.php @@ -0,0 +1,12 @@ + $this->total, + 'monthly' => $this->monthly, + 'daily' => $this->daily, + ]; + } +} diff --git a/app/src/Infrastructure/Packagist/DTO/Github.php b/app/src/Infrastructure/Packagist/DTO/Github.php new file mode 100644 index 0000000..5903ed5 --- /dev/null +++ b/app/src/Infrastructure/Packagist/DTO/Github.php @@ -0,0 +1,26 @@ + $this->stars, + 'watchers' => $this->watchers, + 'forks' => $this->forks, + 'open_issues' => $this->openIssues, + ]; + } +} diff --git a/app/src/Infrastructure/Packagist/DTO/Package.php b/app/src/Infrastructure/Packagist/DTO/Package.php new file mode 100644 index 0000000..b7cf172 --- /dev/null +++ b/app/src/Infrastructure/Packagist/DTO/Package.php @@ -0,0 +1,25 @@ + $this->downloads->jsonSerialize(), + 'github' => $this->github->jsonSerialize(), + ]; + } +} diff --git a/app/src/Infrastructure/Packagist/PackagistConfig.php b/app/src/Infrastructure/Packagist/PackagistConfig.php new file mode 100644 index 0000000..c95f50a --- /dev/null +++ b/app/src/Infrastructure/Packagist/PackagistConfig.php @@ -0,0 +1,26 @@ + [], + ] + ) { + } + + public function getRepositories(): array + { + return $this->config['repositories'] ?? []; + } +} + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..dccf0d5 --- /dev/null +++ b/composer.json @@ -0,0 +1,52 @@ +{ + "name": "metrixio/packagist", + "type": "project", + "license": "MIT", + "description": "This tool lets you easily gather data about downloads from Packagist. It works with Prometheus and Grafana to collect data.", + "homepage": "https://github.com/metrixio/packagist", + "support": { + "issues": "https://github.com/metrixio/packagist/issues", + "source": "https://github.com/metrixio/packagist" + }, + "require": { + "php": ">=8.1", + "ext-mbstring": "*", + "nesbot/carbon": "^2.63", + "spiral/framework": "^3.5", + "spiral/roadrunner-bridge": "^2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "spiral/testing": "^2.2", + "symfony/var-dumper": "^6.1", + "vimeo/psalm": "dev-master" + }, + "autoload": { + "psr-4": { + "App\\": "app/src" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests" + } + }, + "extra": { + "publish-cmd": "php app.php publish" + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "spiral/composer-publish-plugin": true + } + }, + "scripts": { + "post-create-project-cmd": [ + "php -r \"copy('.env.sample', '.env');\"", + "php app.php configure -vv", + "rr get-binary" + ] + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..98efc28 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,33 @@ +# Build rr binary +FROM spiralscout/roadrunner:latest as rr + +# Clone the repository +FROM alpine/git as git + +ARG REPOSITORY=https://github.com/metrixio/packagist.git +ARG BRANCH=master +RUN git clone -b $BRANCH $REPOSITORY /app + +# Configure PHP project +FROM ghcr.io/metrixio/php81-alpine:latest + +COPY --from=git /app /app +COPY --from=rr /usr/bin/rr /app + +ARG APP_VERSION=v1.0 +ENV COMPOSER_ALLOW_SUPERUSER=1 + +RUN curl -s https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin/ --filename=composer +WORKDIR /app + +RUN composer config --no-plugins allow-plugins.spiral/composer-publish-plugin false +RUN composer install --no-dev + +EXPOSE 6001/tcp +EXPOSE 2112/tcp + +LABEL org.opencontainers.image.source=$REPOSITORY +LABEL org.opencontainers.image.description="metrix.io Packagist" +LABEL org.opencontainers.image.licenses=MIT + +CMD ./rr serve diff --git a/grafana/packagist-stat.json b/grafana/packagist-stat.json new file mode 100644 index 0000000..ffb4ce7 --- /dev/null +++ b/grafana/packagist-stat.json @@ -0,0 +1,493 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 8, + "panels": [], + "title": "Stars", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlYlRd" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 0, + "y": 1 + }, + "id": 13, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads_daily{package=\"$repository\"}", + "legendFormat": "{{repo}}", + "range": true, + "refId": "A" + } + ], + "title": "Dailty Downloads", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 4, + "y": 1 + }, + "id": 12, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads_monthly{package=\"$repository\"}", + "legendFormat": "{{package}}", + "range": true, + "refId": "A" + } + ], + "title": "Monthly downloads", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 4, + "x": 8, + "y": 1 + }, + "id": 14, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads{package=\"$repository\"}", + "legendFormat": "{{package}}", + "range": true, + "refId": "A" + } + ], + "title": "Total downloads", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "series", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": -1, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 7, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "predict_linear(packagist_downloads_daily{package=\"$repository\"}[24h], 60)", + "legendFormat": "Daily", + "range": true, + "refId": "A" + } + ], + "title": "Downloads", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "series", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 2, + "pointSize": 7, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 15, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads_daily{package=\"$repository\"}", + "interval": "24h", + "legendFormat": "Daily", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads_monthly{package=\"$repository\"}", + "hide": false, + "interval": "24h", + "legendFormat": "Monthly", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads{package=\"$repository\"}", + "hide": false, + "interval": "24h", + "legendFormat": "Total", + "range": true, + "refId": "C" + } + ], + "title": "Downloads", + "transparent": true, + "type": "timeseries" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "spiral/framework", + "value": "spiral/framework" + }, + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "definition": "packagist_downloads", + "hide": 0, + "includeAll": false, + "multi": false, + "name": "repository", + "options": [], + "query": { + "query": "packagist_downloads", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "/package=\\\"(?[\\w\\/\\-\\_]+)\\\"/", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "title": "Packagist repository stat" +} diff --git a/grafana/packagist.json b/grafana/packagist.json new file mode 100644 index 0000000..4816971 --- /dev/null +++ b/grafana/packagist.json @@ -0,0 +1,278 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": false, + "title": "", + "tooltip": "", + "type": "dashboards", + "url": "" + } + ], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "links": [ + { + "targetBlank": true, + "title": "", + "url": "https://packagist.org/packages/${__field.labels.package}/stats" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": -1 + }, + { + "color": "#EAB839", + "value": 1 + }, + { + "color": "#6ED0E0", + "value": 10 + }, + { + "color": "green", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 6, + "options": { + "colorMode": "background", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name" + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "delta(packagist_downloads_daily[24h])", + "legendFormat": "{{package}}", + "range": true, + "refId": "A" + } + ], + "title": "Daily Installs", + "transparent": true, + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "links": [ + { + "targetBlank": true, + "title": "", + "url": "https://packagist.org/packages/${__field.labels.package}/stats" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 4, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "exemplar": false, + "expr": "packagist_downloads_monthly", + "format": "time_series", + "instant": false, + "legendFormat": "{{package}}", + "range": true, + "refId": "A" + } + ], + "title": "Monthly Installs", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-BlYlRd" + }, + "decimals": 0, + "links": [ + { + "targetBlank": true, + "title": "", + "url": "https://packagist.org/packages/${__field.labels.package}/stats" + } + ], + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 2, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value_and_name" + }, + "pluginVersion": "9.2.5", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "builder", + "expr": "packagist_downloads", + "legendFormat": "{{package}}", + "range": true, + "refId": "A" + } + ], + "title": "Total Installs", + "type": "stat" + } + ], + "style": "dark", + "tags": [], + "title": "Packagist" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..44f91c2 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,33 @@ + + + + + app/src + + + + + tests/Unit + + + tests/Feature + + + app/src/Module/*/Test + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..a7dff04 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + +