Skip to content

Commit

Permalink
Improve README (#7)
Browse files Browse the repository at this point in the history
* adds in_memory example

* adds DataDogStatsDClientAdapter

* create datadog example

* ignore log_datadog.txt

* log_datadog example

* style

* testing sub-section

* spacing

* wrap up README
  • Loading branch information
cosmastech authored Jul 9, 2024
1 parent 13c3fb1 commit 7433aef
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 11 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ vendor/*
composer.lock
.php-cs-fixer.cache
build
.phpunit.cache
.phpunit.cache
examples/log_datadog.txt
60 changes: 57 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,71 @@
[![Latest Stable Version](http://poser.pugx.org/cosmastech/statsd-client-adapter/v)](https://packagist.org/packages/cosmastech/statsd-client-adapter) [![Total Downloads](http://poser.pugx.org/cosmastech/statsd-client-adapter/downloads)](https://packagist.org/packages/cosmastech/statsd-client-adapter) [![License](http://poser.pugx.org/cosmastech/statsd-client-adapter/license)](https://packagist.org/packages/cosmastech/statsd-client-adapter) [![PHP Version Require](http://poser.pugx.org/cosmastech/statsd-client-adapter/require/php)](https://packagist.org/packages/cosmastech/statsd-client-adapter)


# StatsD Client Adapter
This package was originally designed to solve the problem of:
* I use DataDog on production, but
* I don't want to push stats to DataDog on my dev or test environments

Where might I want to push those precious stats? Maybe to a log? Maybe to a locally running [StatsD server](https://github.com/statsd/statsd)? What if in my unit tests, I want to confirm that logs are being pushed, but not go through the hassle of an integration test set up that configures the StatsD server?
Where might I want to push those precious stats? Maybe to a log? Maybe to a locally running [StatsD server](https://github.com/statsd/statsd)?
What if in my unit tests, I want to confirm that logs are being pushed, but not go through the hassle of an integration
test set up that configures the StatsD server?

While [PHP League's statsd package](https://github.com/thephpleague/statsd) is great, it doesn't allow for sending DataDog specific stats
(such as [histogram](https://docs.datadoghq.com/metrics/types/?tab=histogram) or [distribution](https://docs.datadoghq.com/metrics/types/?tab=distribution)).
Nor does the DataDog client allow for pushing to another StatsD implementation easily.

The aim here is to allow for a single interface that can wrap around both, and be easily extended for different implementations.


## Adapters

### InMemoryClientAdapter
This adapter simply records your stats in an object in memory. This is best served as a way to verify stats are recorded in your unit tests.

See [examples/in_memory.php](examples/in_memory.php) for how you might implement this.

### DataDogStatsDClientAdapter
This is a wrapper around DataDog's [php-datadogstatsd](https://github.com/dataDog/php-datadogstatsd/) client.

If you wish to use this adapter, please make sure you install the php-datadogstatsd client.

```shell
composer require datadog/php-datadogstatsd
```

For specifics on their configuration, see the [official DogStatsD documentation](https://docs.datadoghq.com/developers/dogstatsd/?code-lang=php&tab=hostagent#client-instantiation-parameters).

See [examples/datadog.php](examples/datadog.php) for how you might implement this.

### DatadogLoggingClient
Envisioned as a client for local development, this adapter writes to a class which implements the [psr-logger interface](https://packagist.org/packages/psr/log).
You can find a [list](https://packagist.org/providers/psr/log-implementation) of packages that implement the interface on packagist.
If you are using a framework like Symfony or Laravel, then you already have one of the most popular and reliable implementations installed: [monolog/monolog](https://github.com/Seldaek/monolog).

For a local development setup, you could just write the stats to a log. This writes the format exactly as it would be sent to DataDog.

See [examples/log_datadog.php](examples/log_datadog.php) for how you might implement this.

### LeagueStatsDClientAdapter
You can also write to an arbitrary statsd server by leveraging [PHP League's statsd package](https://github.com/thephpleague/statsd).

First ensure that the package has been installed.
```shell
composer require league/statsd
```

For information on how to configure Client, [read their documentation](https://github.com/thephpleague/statsd?tab=readme-ov-file#configuring).

**Note** the `histogram()` and `distribution()` methods are both no-op by default, as they are not available on statsd.

See [examples/league.php](examples/league.php) for how you might implement this.

## Gotchas
1. Only increment/decrement on PHPLeague's implementation allow for including the sample rate. If you are using a sample rate with other calls, their sample rate will not be included as part of the stat.
2. There are `histogram()` and `distribution()` methods on `LeagueStatsDClientAdapter`, but they only raise a PHP error and are no-op.
1. Only increment/decrement on DataDog's implementation allow for including the sample rate. If you are using a sample rate with other calls, their sample rate will not be included as part of the stat.
2. There are `histogram()` and `distribution()` methods on `LeagueStatsDClientAdapter`, but they will not be sent to statsd.


## Testing
```shell
composer test
```
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
],
"require": {
"php": "^8.2",
"datadog/php-datadogstatsd": "^1.6.1",
"psr/clock": "^1.0.0",
"psr/log": "^3.0.0"
},
"require-dev": {
"phpunit/phpunit": "^11.2.5",
"friendsofphp/php-cs-fixer": "^3.59",
"league/statsd": "^2.0.0",
"cosmastech/psr-logger-spy": "^0.0.2"
"cosmastech/psr-logger-spy": "^0.0.2",
"datadog/php-datadogstatsd": "^1.6.1"
},
"suggest": {
"datadog/php-datadogstatsd": "For DataDog stats",
Expand Down
13 changes: 13 additions & 0 deletions examples/datadog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

require_once __DIR__ . "/../vendor/autoload.php";

// See instantiation parameters: https://docs.datadoghq.com/developers/dogstatsd/?code-lang=php&tab=hostagent#client-instantiation-parameters

$datadog = new \Datadog\DogStatsd();

$adapter = new \Cosmastech\StatsDClientAdapter\Adapters\Datadog\DatadogStatsDClientAdapter($datadog);

$adapter->histogram("my-histogram", 11.2);

// Check DataDog and see that this histogram is recorded
53 changes: 53 additions & 0 deletions examples/in_memory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

require_once __DIR__ . "/../vendor/autoload.php";

function timeInMilliseconds()
{
return time() * 1000;
}

function makeApiRequest()
{
sleep(1);
}

$inMemoryAdapter = new \Cosmastech\StatsDClientAdapter\Adapters\InMemory\InMemoryClientAdapter();

$inMemoryAdapter->setDefaultTags(["app_version" => "2.83.0"]); // Set this for tags you want included in all stats.

$startTimeInMs = timeInMilliseconds();
makeApiRequest();

$inMemoryAdapter->timing("api-response", timeInMilliseconds() - $startTimeInMs, 1.0, ["source" => "github"]);

/** @var \Cosmastech\StatsDClientAdapter\Adapters\InMemory\Models\InMemoryStatsRecord $stats */
$stats = $inMemoryAdapter->getStats();

var_dump($stats->timing[0]);
/*
object(Cosmastech\StatsDClientAdapter\Adapters\InMemory\Models\InMemoryTimingRecord)#7 (5) {
["stat"]=>
string(12) "api-response"
["durationMilliseconds"]=>
float(2000)
["sampleRate"]=>
float(1)
["tags"]=>
array(2) {
["app_version"]=>
string(6) "2.83.0"
["source"]=>
string(6) "github"
}
["recordedAt"]=>
object(DateTimeImmutable)#8 (3) {
["date"]=>
string(26) "2024-07-08 22:25:53.080522"
["timezone_type"]=>
int(3)
["timezone"]=>
string(3) "UTC"
}
}
*/
16 changes: 16 additions & 0 deletions examples/league.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

require_once __DIR__ . "/../vendor/autoload.php";

// This requires that the league/statsd package is installed for your project.

$adapter = \Cosmastech\StatsDClientAdapter\Adapters\League\LeagueStatsDClientAdapter::fromConfig([
// See configuration options at https://github.com/thephpleague/statsd?tab=readme-ov-file#configuring
'host' => '127.0.0.1',
'port' => 8125,
'namespace' => 'example',
]);

$adapter->gauge("my-stat", 1.1);

// Confirm in your statsd daemon that the gauge was logged
19 changes: 19 additions & 0 deletions examples/log_datadog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

require_once __DIR__ . "/../vendor/autoload.php";

// See instantiation parameters: https://docs.datadoghq.com/developers/dogstatsd/?code-lang=php&tab=hostagent#client-instantiation-parameters

// You will need Monolog installed to run this example.

$logger = new \Monolog\Logger('log_datadog');
$logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/log_datadog.txt', \Monolog\Level::Debug));

$datadog = new \Cosmastech\StatsDClientAdapter\Clients\Datadog\DatadogLoggingClient($logger);

$adapter = new \Cosmastech\StatsDClientAdapter\Adapters\Datadog\DatadogStatsDClientAdapter($datadog);

$adapter->increment("logins", 1, ["type" => "successful"], 1);

// You should see a file named log_datadog.txt in this directory which will have stats
// ex: [2024-07-08T23:59:18.880180+00:00] log_datadog.DEBUG: logins:1|c|#type:successful [] []
45 changes: 42 additions & 3 deletions src/Adapters/League/LeagueStatsDClientAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace Cosmastech\StatsDClientAdapter\Adapters\League;

use Closure;
use Cosmastech\StatsDClientAdapter\Adapters\Concerns\HasDefaultTagsTrait;
use Cosmastech\StatsDClientAdapter\Adapters\Concerns\TagNormalizerAwareTrait;
use Cosmastech\StatsDClientAdapter\Adapters\Contracts\TagNormalizerAware;
use Cosmastech\StatsDClientAdapter\Adapters\StatsDClientAdapter;
use Cosmastech\StatsDClientAdapter\TagNormalizers\NoopTagNormalizer;
use Cosmastech\StatsDClientAdapter\TagNormalizers\TagNormalizer;
use Cosmastech\StatsDClientAdapter\Utility\SampleRateDecider\Contracts\SampleRateSendDecider as SampleRateSendDeciderInterface;
use Cosmastech\StatsDClientAdapter\Utility\SampleRateDecider\SampleRateSendDecider;
use League\StatsD\Client;
Expand All @@ -18,12 +21,19 @@ class LeagueStatsDClientAdapter implements StatsDClientAdapter, TagNormalizerAwa
use HasDefaultTagsTrait;
use TagNormalizerAwareTrait;

/**
* @var Closure(string, float, float, array<mixed, mixed>):void
*/
protected Closure $unavailableStatHandler;

public function __construct(
protected readonly LeagueStatsDClientInterface $leagueStatsDClient,
protected readonly SampleRateSendDeciderInterface $sampleRateSendDecider,
protected readonly SampleRateSendDeciderInterface $sampleRateSendDecider = new SampleRateSendDecider(),
array $defaultTags = [],
TagNormalizer $tagNormalizer = new NoopTagNormalizer(),
) {
$this->setDefaultTags($defaultTags);
$this->setTagNormalizer($tagNormalizer);
}

/**
Expand All @@ -45,6 +55,35 @@ public static function fromConfig(
);
}

/**
* @param Closure(string, float, float, array<mixed, mixed>):void $closure
* @return self
*/
public function setUnavailableStatHandler(Closure $closure): self
{
$this->unavailableStatHandler = $closure;

return $this;
}

protected function handleUnavailableStat(
string $stat,
float $value,
float $sampleRate = 1.0,
array $tags = []
): void {
$this->getUnavailableStatHandler()($stat, $value, $sampleRate, $tags);
}

/**
* @return Closure(string, float, float, array<mixed, mixed>):void
*/
protected function getUnavailableStatHandler(): Closure
{
return $this->unavailableStatHandler ?? function (): void {};
}


/**
* @throws ConnectionException
*/
Expand Down Expand Up @@ -79,12 +118,12 @@ public function gauge(string $stat, float $value, float $sampleRate = 1.0, array

public function histogram(string $stat, float $value, float $sampleRate = 1.0, array $tags = []): void
{
trigger_error("histogram is not implemented for this client");
$this->handleUnavailableStat($stat, $value, $sampleRate, $tags);
}

public function distribution(string $stat, float $value, float $sampleRate = 1.0, array $tags = []): void
{
trigger_error("distribution is not implemented for this client");
$this->handleUnavailableStat($stat, $value, $sampleRate, $tags);
}

/**
Expand Down
36 changes: 34 additions & 2 deletions tests/Adapters/League/LeagueStatsDClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,48 @@

class LeagueStatsDClientTest extends BaseTestCase
{
protected array $args;
protected function setUp(): void
{
parent::setUp();

$this->args = [];
}

#[Test]
public function getClient_returnsLeagueStatsDClient(): void
{
// Given
$leagueStatsDClient = LeagueStatsDClientAdapter::fromConfig([]);
$leagueStatsDClientAdapter = LeagueStatsDClientAdapter::fromConfig([]);

// When
$client = $leagueStatsDClient->getClient();
$client = $leagueStatsDClientAdapter->getClient();

// Then
self::assertInstanceOf(StatsDClient::class, $client);
}

#[Test]
public function setUnavailableStatHandler_histogram_callsClosure(): void
{
// Given
$leagueStatsDClientAdapter = LeagueStatsDClientAdapter::fromConfig([]);

// And
$leagueStatsDClientAdapter->setUnavailableStatHandler($this->saveArgs(...));

// When
$leagueStatsDClientAdapter->histogram("some-stat", 12, 0.1, ["my_tag" => true]);

// Then
self::assertEquals("some-stat", $this->args[0]);
self::assertEquals(12, $this->args[1]);
self::assertEquals(0.1, $this->args[2]);
self::assertEquals(["my_tag" => true], $this->args[3]);
}

private function saveArgs(): void
{
$this->args = func_get_args();
}
}

0 comments on commit 7433aef

Please sign in to comment.