Skip to content

Commit 7433aef

Browse files
authored
Improve README (#7)
* adds in_memory example * adds DataDogStatsDClientAdapter * create datadog example * ignore log_datadog.txt * log_datadog example * style * testing sub-section * spacing * wrap up README
1 parent 13c3fb1 commit 7433aef

File tree

9 files changed

+238
-11
lines changed

9 files changed

+238
-11
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ vendor/*
33
composer.lock
44
.php-cs-fixer.cache
55
build
6-
.phpunit.cache
6+
.phpunit.cache
7+
examples/log_datadog.txt

README.md

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,71 @@
11
[![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)
2+
3+
24
# StatsD Client Adapter
35
This package was originally designed to solve the problem of:
46
* I use DataDog on production, but
57
* I don't want to push stats to DataDog on my dev or test environments
68

7-
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?
9+
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)?
10+
What if in my unit tests, I want to confirm that logs are being pushed, but not go through the hassle of an integration
11+
test set up that configures the StatsD server?
812

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

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

19+
20+
## Adapters
21+
22+
### InMemoryClientAdapter
23+
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.
24+
25+
See [examples/in_memory.php](examples/in_memory.php) for how you might implement this.
26+
27+
### DataDogStatsDClientAdapter
28+
This is a wrapper around DataDog's [php-datadogstatsd](https://github.com/dataDog/php-datadogstatsd/) client.
29+
30+
If you wish to use this adapter, please make sure you install the php-datadogstatsd client.
31+
32+
```shell
33+
composer require datadog/php-datadogstatsd
34+
```
35+
36+
For specifics on their configuration, see the [official DogStatsD documentation](https://docs.datadoghq.com/developers/dogstatsd/?code-lang=php&tab=hostagent#client-instantiation-parameters).
37+
38+
See [examples/datadog.php](examples/datadog.php) for how you might implement this.
39+
40+
### DatadogLoggingClient
41+
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).
42+
You can find a [list](https://packagist.org/providers/psr/log-implementation) of packages that implement the interface on packagist.
43+
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).
44+
45+
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.
46+
47+
See [examples/log_datadog.php](examples/log_datadog.php) for how you might implement this.
48+
49+
### LeagueStatsDClientAdapter
50+
You can also write to an arbitrary statsd server by leveraging [PHP League's statsd package](https://github.com/thephpleague/statsd).
51+
52+
First ensure that the package has been installed.
53+
```shell
54+
composer require league/statsd
55+
```
56+
57+
For information on how to configure Client, [read their documentation](https://github.com/thephpleague/statsd?tab=readme-ov-file#configuring).
58+
59+
**Note** the `histogram()` and `distribution()` methods are both no-op by default, as they are not available on statsd.
60+
61+
See [examples/league.php](examples/league.php) for how you might implement this.
62+
1563
## Gotchas
16-
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.
17-
2. There are `histogram()` and `distribution()` methods on `LeagueStatsDClientAdapter`, but they only raise a PHP error and are no-op.
64+
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.
65+
2. There are `histogram()` and `distribution()` methods on `LeagueStatsDClientAdapter`, but they will not be sent to statsd.
66+
67+
68+
## Testing
69+
```shell
70+
composer test
71+
```

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@
1111
],
1212
"require": {
1313
"php": "^8.2",
14-
"datadog/php-datadogstatsd": "^1.6.1",
1514
"psr/clock": "^1.0.0",
1615
"psr/log": "^3.0.0"
1716
},
1817
"require-dev": {
1918
"phpunit/phpunit": "^11.2.5",
2019
"friendsofphp/php-cs-fixer": "^3.59",
2120
"league/statsd": "^2.0.0",
22-
"cosmastech/psr-logger-spy": "^0.0.2"
21+
"cosmastech/psr-logger-spy": "^0.0.2",
22+
"datadog/php-datadogstatsd": "^1.6.1"
2323
},
2424
"suggest": {
2525
"datadog/php-datadogstatsd": "For DataDog stats",

examples/datadog.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
require_once __DIR__ . "/../vendor/autoload.php";
4+
5+
// See instantiation parameters: https://docs.datadoghq.com/developers/dogstatsd/?code-lang=php&tab=hostagent#client-instantiation-parameters
6+
7+
$datadog = new \Datadog\DogStatsd();
8+
9+
$adapter = new \Cosmastech\StatsDClientAdapter\Adapters\Datadog\DatadogStatsDClientAdapter($datadog);
10+
11+
$adapter->histogram("my-histogram", 11.2);
12+
13+
// Check DataDog and see that this histogram is recorded

examples/in_memory.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
require_once __DIR__ . "/../vendor/autoload.php";
4+
5+
function timeInMilliseconds()
6+
{
7+
return time() * 1000;
8+
}
9+
10+
function makeApiRequest()
11+
{
12+
sleep(1);
13+
}
14+
15+
$inMemoryAdapter = new \Cosmastech\StatsDClientAdapter\Adapters\InMemory\InMemoryClientAdapter();
16+
17+
$inMemoryAdapter->setDefaultTags(["app_version" => "2.83.0"]); // Set this for tags you want included in all stats.
18+
19+
$startTimeInMs = timeInMilliseconds();
20+
makeApiRequest();
21+
22+
$inMemoryAdapter->timing("api-response", timeInMilliseconds() - $startTimeInMs, 1.0, ["source" => "github"]);
23+
24+
/** @var \Cosmastech\StatsDClientAdapter\Adapters\InMemory\Models\InMemoryStatsRecord $stats */
25+
$stats = $inMemoryAdapter->getStats();
26+
27+
var_dump($stats->timing[0]);
28+
/*
29+
object(Cosmastech\StatsDClientAdapter\Adapters\InMemory\Models\InMemoryTimingRecord)#7 (5) {
30+
["stat"]=>
31+
string(12) "api-response"
32+
["durationMilliseconds"]=>
33+
float(2000)
34+
["sampleRate"]=>
35+
float(1)
36+
["tags"]=>
37+
array(2) {
38+
["app_version"]=>
39+
string(6) "2.83.0"
40+
["source"]=>
41+
string(6) "github"
42+
}
43+
["recordedAt"]=>
44+
object(DateTimeImmutable)#8 (3) {
45+
["date"]=>
46+
string(26) "2024-07-08 22:25:53.080522"
47+
["timezone_type"]=>
48+
int(3)
49+
["timezone"]=>
50+
string(3) "UTC"
51+
}
52+
}
53+
*/

examples/league.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
require_once __DIR__ . "/../vendor/autoload.php";
4+
5+
// This requires that the league/statsd package is installed for your project.
6+
7+
$adapter = \Cosmastech\StatsDClientAdapter\Adapters\League\LeagueStatsDClientAdapter::fromConfig([
8+
// See configuration options at https://github.com/thephpleague/statsd?tab=readme-ov-file#configuring
9+
'host' => '127.0.0.1',
10+
'port' => 8125,
11+
'namespace' => 'example',
12+
]);
13+
14+
$adapter->gauge("my-stat", 1.1);
15+
16+
// Confirm in your statsd daemon that the gauge was logged

examples/log_datadog.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
require_once __DIR__ . "/../vendor/autoload.php";
4+
5+
// See instantiation parameters: https://docs.datadoghq.com/developers/dogstatsd/?code-lang=php&tab=hostagent#client-instantiation-parameters
6+
7+
// You will need Monolog installed to run this example.
8+
9+
$logger = new \Monolog\Logger('log_datadog');
10+
$logger->pushHandler(new \Monolog\Handler\StreamHandler(__DIR__ . '/log_datadog.txt', \Monolog\Level::Debug));
11+
12+
$datadog = new \Cosmastech\StatsDClientAdapter\Clients\Datadog\DatadogLoggingClient($logger);
13+
14+
$adapter = new \Cosmastech\StatsDClientAdapter\Adapters\Datadog\DatadogStatsDClientAdapter($datadog);
15+
16+
$adapter->increment("logins", 1, ["type" => "successful"], 1);
17+
18+
// You should see a file named log_datadog.txt in this directory which will have stats
19+
// ex: [2024-07-08T23:59:18.880180+00:00] log_datadog.DEBUG: logins:1|c|#type:successful [] []

src/Adapters/League/LeagueStatsDClientAdapter.php

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace Cosmastech\StatsDClientAdapter\Adapters\League;
44

5+
use Closure;
56
use Cosmastech\StatsDClientAdapter\Adapters\Concerns\HasDefaultTagsTrait;
67
use Cosmastech\StatsDClientAdapter\Adapters\Concerns\TagNormalizerAwareTrait;
78
use Cosmastech\StatsDClientAdapter\Adapters\Contracts\TagNormalizerAware;
89
use Cosmastech\StatsDClientAdapter\Adapters\StatsDClientAdapter;
10+
use Cosmastech\StatsDClientAdapter\TagNormalizers\NoopTagNormalizer;
11+
use Cosmastech\StatsDClientAdapter\TagNormalizers\TagNormalizer;
912
use Cosmastech\StatsDClientAdapter\Utility\SampleRateDecider\Contracts\SampleRateSendDecider as SampleRateSendDeciderInterface;
1013
use Cosmastech\StatsDClientAdapter\Utility\SampleRateDecider\SampleRateSendDecider;
1114
use League\StatsD\Client;
@@ -18,12 +21,19 @@ class LeagueStatsDClientAdapter implements StatsDClientAdapter, TagNormalizerAwa
1821
use HasDefaultTagsTrait;
1922
use TagNormalizerAwareTrait;
2023

24+
/**
25+
* @var Closure(string, float, float, array<mixed, mixed>):void
26+
*/
27+
protected Closure $unavailableStatHandler;
28+
2129
public function __construct(
2230
protected readonly LeagueStatsDClientInterface $leagueStatsDClient,
23-
protected readonly SampleRateSendDeciderInterface $sampleRateSendDecider,
31+
protected readonly SampleRateSendDeciderInterface $sampleRateSendDecider = new SampleRateSendDecider(),
2432
array $defaultTags = [],
33+
TagNormalizer $tagNormalizer = new NoopTagNormalizer(),
2534
) {
2635
$this->setDefaultTags($defaultTags);
36+
$this->setTagNormalizer($tagNormalizer);
2737
}
2838

2939
/**
@@ -45,6 +55,35 @@ public static function fromConfig(
4555
);
4656
}
4757

58+
/**
59+
* @param Closure(string, float, float, array<mixed, mixed>):void $closure
60+
* @return self
61+
*/
62+
public function setUnavailableStatHandler(Closure $closure): self
63+
{
64+
$this->unavailableStatHandler = $closure;
65+
66+
return $this;
67+
}
68+
69+
protected function handleUnavailableStat(
70+
string $stat,
71+
float $value,
72+
float $sampleRate = 1.0,
73+
array $tags = []
74+
): void {
75+
$this->getUnavailableStatHandler()($stat, $value, $sampleRate, $tags);
76+
}
77+
78+
/**
79+
* @return Closure(string, float, float, array<mixed, mixed>):void
80+
*/
81+
protected function getUnavailableStatHandler(): Closure
82+
{
83+
return $this->unavailableStatHandler ?? function (): void {};
84+
}
85+
86+
4887
/**
4988
* @throws ConnectionException
5089
*/
@@ -79,12 +118,12 @@ public function gauge(string $stat, float $value, float $sampleRate = 1.0, array
79118

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

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

90129
/**

tests/Adapters/League/LeagueStatsDClientTest.php

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,48 @@
99

1010
class LeagueStatsDClientTest extends BaseTestCase
1111
{
12+
protected array $args;
13+
protected function setUp(): void
14+
{
15+
parent::setUp();
16+
17+
$this->args = [];
18+
}
19+
1220
#[Test]
1321
public function getClient_returnsLeagueStatsDClient(): void
1422
{
1523
// Given
16-
$leagueStatsDClient = LeagueStatsDClientAdapter::fromConfig([]);
24+
$leagueStatsDClientAdapter = LeagueStatsDClientAdapter::fromConfig([]);
1725

1826
// When
19-
$client = $leagueStatsDClient->getClient();
27+
$client = $leagueStatsDClientAdapter->getClient();
2028

2129
// Then
2230
self::assertInstanceOf(StatsDClient::class, $client);
2331
}
32+
33+
#[Test]
34+
public function setUnavailableStatHandler_histogram_callsClosure(): void
35+
{
36+
// Given
37+
$leagueStatsDClientAdapter = LeagueStatsDClientAdapter::fromConfig([]);
38+
39+
// And
40+
$leagueStatsDClientAdapter->setUnavailableStatHandler($this->saveArgs(...));
41+
42+
// When
43+
$leagueStatsDClientAdapter->histogram("some-stat", 12, 0.1, ["my_tag" => true]);
44+
45+
// Then
46+
self::assertEquals("some-stat", $this->args[0]);
47+
self::assertEquals(12, $this->args[1]);
48+
self::assertEquals(0.1, $this->args[2]);
49+
self::assertEquals(["my_tag" => true], $this->args[3]);
50+
}
51+
52+
private function saveArgs(): void
53+
{
54+
$this->args = func_get_args();
55+
}
2456
}

0 commit comments

Comments
 (0)