Skip to content

Ability to use TCP connections. #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Latest Stable Version](https://poser.pugx.org/league/statsd/v/stable.png)](https://packagist.org/packages/league/statsd)


A library for working with StatsD in PHP.
A framework-agnostic library for working with StatsD in PHP.



Expand Down Expand Up @@ -53,6 +53,23 @@ to disable these exceptions and log a PHP warning instead if you wish. To do so,
If omitted, this option defaults to `true`.


### Configuring to use TCP

***Attention!** With a TCP port your application will slow down. Use it if you know what you are doing.*

By default, StatsD client use UDP port. In most cases you won't need anything else. But it's also possible to use
TCP port. Just provide the desired scheme name in your configuration.

```php
$statsd = new League\StatsD\Client();
$statsd->configure([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 8125
]);
```

TCP connection allows you to send a huge bunches of metrics in single call. It also has delivery guarantees.

### Counters

Expand Down Expand Up @@ -144,7 +161,7 @@ Find the `aliases` key in your `app/config/app.php` and add the Statsd Facade Al
'Statsd' => 'League\StatsD\Laravel\Facade\StatsdFacade',
]
```
### Laravel 5.x
### Laravel 5.x and greater

If you are using Laravel `>=5.5`, statsd uses [package discovery](https://laravel.com/docs/5.5/packages#package-discovery) to automatically register the service provider and facade.

Expand Down Expand Up @@ -185,6 +202,7 @@ Package Configuration
In your `.env` file, add the configuration:

```php
STATSD_SCHEME=udp
STATSD_HOST=127.0.0.1
STATSD_PORT=8125
STATSD_NAMESPACE=
Expand Down
4 changes: 3 additions & 1 deletion config/statsd.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<?php

return [
'host' => env('STATSD_HOST', '127.0.0.1'),
'scheme' => env('STATSD_SCHEME', 'udp'),

'host' => env('STATSD_HOST', '127.0.0.1'),

'port' => env('STATSD_PORT', 8125),

Expand Down
4 changes: 4 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,8 @@
</testsuite>
</testsuites>

<php>
<env name="STATSD_TCP_ADDRESS" value=""/>
</php>

</phpunit>
109 changes: 94 additions & 15 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class Client implements StatsDClient
/** Instance ID */
protected string $instanceId;

/** Server Scheme */
protected string $scheme = 'udp';

/** Server Host */
protected string $host = '127.0.0.1';

Expand All @@ -40,7 +43,7 @@ class Client implements StatsDClient
protected array $metricTiming;

/** @var resource|false|null Socket pointer for sending metrics */
protected $socket;
protected $socket = null;

/** Generic tags */
protected array $tags = [];
Expand All @@ -57,6 +60,16 @@ public static function instance(string $name = 'default'): StatsDClient
return self::$instances[$name];
}

/**
* Forget singleton reference.
*
* @param string $name Name of the instance.
*/
public static function forget(string $name = 'default'): void
{
unset(self::$instances[$name]);
}

/**
* Create a new instance
*/
Expand All @@ -68,6 +81,29 @@ public function __construct(?string $instanceId = null)
}
}

/**
* Destroying an instance.
*/
public function __destruct()
{
if ($this->socket) {
fclose($this->socket);
}
}

/**
* Clone
*/
public function __clone()
{
// Two objects should not use the same resource, or we will get into troubles after destroying of either object.
// So we just unset any existing socket, so it could be created again when needed.
$this->socket = null;
}

/**
* String representation of the instance
*/
public function __toString(): string
{
return 'StatsD\Client::[' . $this->instanceId . ']';
Expand All @@ -79,13 +115,21 @@ public function __toString(): string
* @param array $options Configuration options
*
* @return Client This instance
* @throws ConfigurationException If port is invalid
* @throws ConfigurationException If port or scheme is invalid
*/
public function configure(array $options = []): self
{
if (isset($options['scheme'])) {
if (! is_string($options['scheme']) || ! in_array(strtolower($options['scheme']), ['tcp', 'udp'])) {
throw new ConfigurationException($this, 'Provided scheme is not supported');
}
$this->scheme = strtolower($options['scheme']);
}

if (isset($options['host'])) {
$this->host = $options['host'];
}

if (isset($options['port'])) {
if (! is_numeric($options['port']) || is_float($options['port']) || $options['port'] < 0 || $options['port'] > 65535) {
throw new ConfigurationException($this, 'Port is out of range');
Expand All @@ -112,16 +156,41 @@ public function configure(array $options = []): self
return $this;
}

/**
* Get server scheme
*
* @return string
*/
public function getScheme(): string
{
return $this->scheme;
}

/**
* Get server host
*
* @return string
*/
public function getHost(): string
{
return $this->host;
}

/**
* Get server port
*
* @return int
*/
public function getPort(): int
{
return $this->port;
}

/**
* Get metrics namespace
*
* @return string
*/
public function getNamespace(): string
{
return $this->namespace;
Expand Down Expand Up @@ -169,7 +238,7 @@ public function increment($metrics, int $delta = 1, float $sampleRate = 1, array
*
* @param string|array $metrics Metric(s) to decrement
* @param int $delta Value to increment the metric by
* @param float $sampleRate Sample rate of metric
* @param float $sampleRate Sample rate of metric
* @param array $tags A list of metric tags values
*
* @throws ConnectionException
Expand Down Expand Up @@ -258,13 +327,12 @@ public function time(string $metric, $func, array $tags = []): void
$this->timing($metric, $time, $tags);
}


/**
* Gauges
*
* @param string $metric Metric to gauge
* @param string $metric Metric to gauge
* @param int|float $value Set the value of the gauge
* @param array $tags A list of metric tags values
* @param array $tags A list of metric tags values
*
* @throws ConnectionException
*/
Expand All @@ -276,9 +344,9 @@ public function gauge(string $metric, $value, array $tags = []): void
/**
* Sets - count the number of unique values passed to a key
*
* @param string $metric
* @param mixed $value
* @param array $tags A list of metric tags values
* @param string $metric Metric name
* @param mixed $value Value to count
* @param array $tags A list of metric tags values
*
* @throws ConnectionException
*/
Expand All @@ -288,13 +356,16 @@ public function set(string $metric, $value, array $tags = []): void
}

/**
* @return resource
* @throws ConnectionException
* Get stream to server
*
* @return resource File pointer representing the connection to the server
*
* @throws ConnectionException If there is a connection problem with the host
*/
protected function getSocket()
{
if (! $this->socket) {
$this->socket = @fsockopen('udp://' . $this->host, $this->port, $errno, $errstr, $this->timeout);
$this->socket = @fsockopen($this->scheme . '://' . $this->host, $this->port, $errno, $errstr, $this->timeout);
if (! $this->socket) {
throw new ConnectionException($this, '(' . $errno . ') ' . $errstr);
}
Expand All @@ -303,9 +374,16 @@ protected function getSocket()
return $this->socket;
}

/**
* Serialize tags
*
* @param array $tags A list of tags to send to the server
*
* @return string A string containing a Datadog formatted tags
*/
protected function serializeTags(array $tags): string
{
if (! is_array($tags) || count($tags) === 0) {
if (count($tags) === 0) {
return '';
}
$data = [];
Expand Down Expand Up @@ -335,7 +413,7 @@ protected function send(array $data, array $tags = []): void
foreach ($data as $key => $value) {
$messages[] = $prefix . $key . ':' . $value . $tagsData;
}
$this->message = implode("\n", $messages);
$this->message = implode("\n", $messages) . ($this->scheme === 'tcp' ? "\n" : '');
@fwrite($socket, $this->message);
fflush($socket);
} catch (ConnectionException $e) {
Expand All @@ -344,7 +422,8 @@ protected function send(array $data, array $tags = []): void
} else {
trigger_error(
sprintf(
'StatsD server connection failed (udp://%s:%d): %s',
'StatsD server connection failed (%s://%s:%d): %s',
$this->scheme,
$this->host,
$this->port,
$e->getMessage()
Expand Down
4 changes: 4 additions & 0 deletions src/Laravel/Provider/StatsdServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ function ($app) {
$options = [];
$config = $app['config'];

if (!empty($config['statsd.scheme'])) {
$options['scheme'] = $config['statsd.scheme'];
}

if (isset($config['statsd.host'])) {
$options['host'] = $config['statsd.host'];
}
Expand Down
4 changes: 4 additions & 0 deletions src/Laravel5/Provider/StatsdServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function ($app) {
$options = [];
$config = $app['config'];

if (!empty($config['statsd.scheme'])) {
$options['scheme'] = $config['statsd.scheme'];
}

if (isset($config['statsd.host'])) {
$options['host'] = $config['statsd.host'];
}
Expand Down
26 changes: 23 additions & 3 deletions tests/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,41 @@

class ClientTest extends TestCase
{
/**
* Creates instances of Client
*/
public function testNewInstance()
{
$client = new Client();
$this->assertInstanceOf(Client::class, $client);
$this->assertMatchesRegularExpression('/^StatsD\\\Client::\[[a-zA-Z0-9]+\]$/', (string) $client);
$this->assertMatchesRegularExpression('/^StatsD\\\Client::\[[a-zA-Z0-9]+]$/', (string) $client);
}

/**
* Returns the same instance for the same name
*/
public function testStaticInstance()
{
$client1 = Client::instance('instance1');
$this->assertInstanceOf(Client::class, $client1);
$client2 = Client::instance('instance2');
$client3 = Client::instance('instance1');
$this->assertEquals('StatsD\Client::[instance2]', (string) $client2);
$this->assertFalse((string) $client1 === (string) $client2);
$this->assertTrue((string) $client1 === (string) $client3);
$this->assertFalse($client1 === $client2);
$this->assertTrue($client1 === $client3);
}

/**
* Can forget only specified instance
*/
public function testForgetStaticInstance()
{
$client1 = Client::instance('instance1');
$client2 = Client::instance('instance2');
Client::forget('instance1');
$client3 = Client::instance('instance1');
$client4 = Client::instance('instance2');
$this->assertFalse($client1 === $client3);
$this->assertTrue($client2 === $client4);
}
}
Loading