diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..27f1522 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,50 @@ +name: PHP Composer + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.2" + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Run test suite + run: composer test + + - name: Style check + run: composer php-cs-fixer-check + + - name: Document facade + run: composer facade-documentor-lint + + - name: Static Analysis + run: composer static-analysis diff --git a/composer.json b/composer.json index dce0af8..54819f2 100644 --- a/composer.json +++ b/composer.json @@ -1,43 +1,78 @@ { - "name": "cosmastech/laravel-statsd-adapter", - "description": "Easily use statsd-client-adapter within your Laravel project", - "license": "wtfpl", - "authors": [ - { - "name": "Luke Kuzmish", - "email": "luke@kuzmish.com", - "role": "Developer" + "name": "cosmastech/laravel-statsd-adapter", + "description": "Easily use statsd-client-adapter within your Laravel project", + "license": "wtfpl", + "authors": [ + { + "name": "Luke Kuzmish", + "email": "luke@kuzmish.com", + "role": "Developer" + } + ], + "autoload": { + "psr-4": { + "Cosmastech\\LaravelStatsDAdapter\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "Cosmastech\\LaravelStatsDAdapter\\Tests\\": "tests", + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + } + }, + "require": { + "php": "^8.2", + "cosmastech/statsd-client-adapter": "^0.0.2", + "illuminate/support": "^10.0|^11.0", + "illuminate/contracts": "^10.0|^11.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.2.5", + "friendsofphp/php-cs-fixer": "^3.59", + "league/statsd": "^2.0.0", + "orchestra/testbench": "^9.1", + "laravel/facade-documenter": "dev-main", + "phpstan/phpstan": "^1.11" + }, + "extra": { + "laravel": { + "providers": [ + "Cosmastech\\LaravelStatsDAdapter\\StatsDAdapterServiceProvider" + ] + } + }, + "scripts": { + "test": "phpunit tests", + "php-cs-fixer": "./vendor/bin/php-cs-fixer fix ./", + "php-cs-fixer-check": "./vendor/bin/php-cs-fixer check ./", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "build": "@php vendor/bin/testbench workbench:build --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve" + ], + "static-analysis": [ + "@php vendor/bin/phpstan analyse -c phpstan.neon" + ], + "facade-documentor": [ + "@php -f vendor/bin/facade.php -- \"\\\\Cosmastech\\\\LaravelStatsDAdapter\\\\Stats\"" + ], + "facade-documentor-lint": [ + "@php -f vendor/bin/facade.php -- --lint \"\\\\Cosmastech\\\\LaravelStatsDAdapter\\\\Stats\"" + ] + }, + "repositories": { + "facade-documenter": { + "type": "vcs", + "url": "git@github.com:laravel/facade-documenter.git" + } } - ], - "autoload": { - "psr-4": { - "Cosmastech\\LaravelStatsDAdapter\\": "src" - } - }, - "autoload-dev": { - "psr-4": { - "Cosmastech\\LaravelStatsDAdapter\\Tests\\": "tests" - } - }, - "require": { - "php": "^8.2", - "cosmastech/statsd-client-adapter": "^0.0.1", - "illuminate/support": "^10.0|^11.0" - }, - "require-dev": { - "phpunit/phpunit": "^11.2.5", - "friendsofphp/php-cs-fixer": "^3.59" - }, - "extra": { - "laravel": { - "providers": [ - "Cosmastech\\LaravelStatsDAdapter\\LaravelStatsDAdapterServiceProvider" - ] - } - }, - "scripts": { - "test": "phpunit tests", - "php-cs-fixer": "./vendor/bin/php-cs-fixer fix ./", - "php-cs-fixer-check": "./vendor/bin/php-cs-fixer check ./" - } -} \ No newline at end of file +} diff --git a/config/statsd-adapter.php b/config/statsd-adapter.php new file mode 100644 index 0000000..8674d17 --- /dev/null +++ b/config/statsd-adapter.php @@ -0,0 +1,45 @@ + env("STATSD_ADAPTER_DEFAULT", "memory"), + + /** + * You may name your channel anything you wish. Valid drivers are: + * memory + * league + * datadog + * log_datadog + */ + "channels" => [ + "memory" => [ + "adapter" => "memory", + ], + "league" => [ + "adapter" => "league", + "instance_id" => null, + 'host' => env('STATSD_HOST', '127.0.0.1'), + 'port' => env('STATSD_PORT', 8125), + 'namespace' => env('STATSD_NAMESPACE', ''), + 'throwConnectionExceptions' => true, + ], + "datadog" => [ + "adapter" => "datadog", + "host" => env("DD_AGENT_HOST"), + "port" => env("DD_DOGSTATSD_PORT"), + "socket_path" => null, + "datadog_host" => null, + "decimal_precision" => null, + "global_tags" => [], + "metric_prefix" => null, + "disable_telemetry" => null, + ], + "log_datadog" => [ + "adapter" => "log_datadog", + "log_level" => "debug", + "decimal_precision" => null, + "global_tags" => [], + "metric_prefix" => null, + "disable_telemetry" => null, + ], + ], +]; diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..3a1f859 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 7 + paths: + - src + - tests diff --git a/src/AdapterManager.php b/src/AdapterManager.php new file mode 100644 index 0000000..f61f31e --- /dev/null +++ b/src/AdapterManager.php @@ -0,0 +1,122 @@ +config = $this->app->get('config'); + } + + /** + * @inheritDoc + */ + public function getDefaultInstance() + { + return $this->defaultInstanceName ?? $this->config->get("statsd-adapter.default"); + } + + /** + * @inheritDoc + */ + public function setDefaultInstance($name) + { + $this->defaultInstanceName = $name; + } + + /** + * @return array|null + */ + public function getInstanceConfig($name) + { + return $this->config->get("statsd-adapter.channels.{$name}"); + } + + /** + * @param array $config + * @return InMemoryClientAdapter + */ + protected function createMemoryAdapter(array $config): InMemoryClientAdapter + { + $wrapperClock = new WrapperClock(FactoryImmutable::getDefaultInstance()); + + return new InMemoryClientAdapter($wrapperClock); + } + + /** + * @param array $config + * @return DatadogStatsDClientAdapter + */ + protected function createLog_datadogAdapter(array $config): DatadogStatsDClientAdapter + { + return $this->createLogDatadogAdapter($config); + } + + /** + * @param array $config + * @return DatadogStatsDClientAdapter + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + protected function createLogDatadogAdapter(array $config): DatadogStatsDClientAdapter + { + $logLevel = $config['log_level'] ?? 'debug'; + + return new DatadogStatsDClientAdapter( + new DatadogLoggingClient($this->app->make('log'), $logLevel, $config) + ); + } + + /** + * @param array $config + * @return DatadogStatsDClientAdapter + */ + protected function createDatadogAdapter(array $config): DatadogStatsDClientAdapter + { + return new DatadogStatsDClientAdapter(new DogStatsd($config)); + } + + /** + * @param array $config + * @return LeagueStatsDClientAdapter + * @throws ConfigurationException + */ + protected function createLeagueAdapter(array $config): LeagueStatsDClientAdapter + { + $leagueClient = new Client($config['instance_id'] ?? null); + $leagueClient->configure($config); + + return new LeagueStatsDClientAdapter($leagueClient, new SampleRateSendDecider()); + } +} diff --git a/src/Stats.php b/src/Stats.php new file mode 100644 index 0000000..a05518f --- /dev/null +++ b/src/Stats.php @@ -0,0 +1,33 @@ +app->singleton( + AdapterManager::class, + fn ($app) => new AdapterManager($app) + ); + + $this->app->singleton( + StatsDClientAdapter::class, + fn ($app) => $app->make(AdapterManager::class)->instance() + ); + } + + /** + * @return array + */ + public function provides(): array + { + return [AdapterManager::class, StatsDClientAdapter::class]; + } +} diff --git a/testbench.yaml b/testbench.yaml new file mode 100644 index 0000000..4843c84 --- /dev/null +++ b/testbench.yaml @@ -0,0 +1,22 @@ +providers: + # - Workbench\App\Providers\WorkbenchServiceProvider + +migrations: + - workbench/database/migrations + +seeders: + - Workbench\Database\Seeders\DatabaseSeeder + +workbench: + start: '/' + install: true + health: false + discovers: + web: true + api: false + commands: false + components: false + views: false + build: [] + assets: [] + sync: [] diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php new file mode 100644 index 0000000..924f948 --- /dev/null +++ b/tests/AbstractTestCase.php @@ -0,0 +1,26 @@ +make('config'); + + $config->set('statsd-adapter', include __DIR__ . '/../config/statsd-adapter.php'); + } + + protected function createAdapterManager(): AdapterManager + { + return new AdapterManager($this->app); + } +} diff --git a/tests/AdapterManagerTest.php b/tests/AdapterManagerTest.php new file mode 100644 index 0000000..ef7fe2f --- /dev/null +++ b/tests/AdapterManagerTest.php @@ -0,0 +1,92 @@ +createAdapterManager(); + + // And + Config::set("statsd-adapter.default", "this-does-not-exist-but-thats-ok"); + + // When + $defaultInstanceString = $adapterManager->getDefaultInstance(); + + // Then + self::assertEquals("this-does-not-exist-but-thats-ok", $defaultInstanceString); + } + + #[Test] + public function setDefaultInstance_overridesConfigDefault(): void + { + // Given + $adapterManager = $this->createAdapterManager(); + + // When + $adapterManager->setDefaultInstance("yooooo"); + + // Then + self::assertEquals("yooooo", $adapterManager->getDefaultInstance()); + } + + #[Test] + public function memoryAdapter_instance_returnsInMemoryClientAdapter(): void + { + // Given + $adapterManager = $this->createAdapterManager(); + + // And + Config::set("statsd-adapter.default", "memory"); + + // When + /** @var InMemoryClientAdapter $inMemoryClientAdapter */ + $inMemoryClientAdapter = $adapterManager->instance(); + + // Then + self::assertInstanceOf(InMemoryClientAdapter::class, $inMemoryClientAdapter); + } + + #[Test] + public function logDatadog_instance_returnsConfiguredDatadogClient(): void + { + // Given + $adapterManager = $this->createAdapterManager(); + + // And "log_datadog" channel is set and app is booted + + // When + $datadogClientAdapter = $adapterManager->instance("log_datadog"); + + // Then + self::assertInstanceOf(DatadogStatsDClientAdapter::class, $datadogClientAdapter); + self::assertInstanceOf(DatadogLoggingClient::class, $datadogClientAdapter->getClient()); + } + + public function league_instance_returnsConfiguredLeagueStatsDClient(): void + { + // Given + $adapterManager = $this->createAdapterManager(); + + // And "league" channel is set and app is booted + + // When + /** @var LeagueStatsDClientAdapter $leagueStatsDClientAdapter */ + $leagueStatsDClientAdapter = $adapterManager->instance("league"); + + // Then + self::assertInstanceOf(LeagueStatsDClientAdapter::class, $leagueStatsDClientAdapter); + self::assertInstanceOf(StatsDClient::class, $leagueStatsDClientAdapter->getClient()); + } +} diff --git a/tests/StatsTest.php b/tests/StatsTest.php new file mode 100644 index 0000000..294d598 --- /dev/null +++ b/tests/StatsTest.php @@ -0,0 +1,47 @@ +increment("some-stat"); + + // Then + $stats = $inMemoryClientAdapter->getStats(); + self::assertEqualsWithDelta( + (new DateTimeImmutable("2021-01-01 00:00:00"))->getTimestamp(), + $stats->count[0]->recordedAt->getTimestamp(), + 1 + ); + } +} diff --git a/workbench/bootstrap/app.php b/workbench/bootstrap/app.php new file mode 100644 index 0000000..450b9e9 --- /dev/null +++ b/workbench/bootstrap/app.php @@ -0,0 +1,15 @@ +withMiddleware(function (Middleware $middleware) { + // + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/workbench/bootstrap/providers.php b/workbench/bootstrap/providers.php new file mode 100644 index 0000000..97d53ee --- /dev/null +++ b/workbench/bootstrap/providers.php @@ -0,0 +1,5 @@ +