Skip to content

Commit

Permalink
add: prometheus driver (#20)
Browse files Browse the repository at this point in the history
* upd: wip prometheus support

* upd: wip. basic prometheus counter metric support

* upd: wip. flush and get as prometheus formatted text

* upd: cleanup composer. add more tests. increase php version requirement to use enums.

* add: description prop to metric. wipe registry before formatting. update readme

* upd: driver cleanup and description assertion in test

* upd: format underscore all metric names and labels. fix tests

* fix: readme

* upd: remove not needed dev dependencies, we have them in suggestions so its up to the client which one to install
  • Loading branch information
dmtar authored Feb 15, 2024
1 parent 6e64912 commit fe1b99d
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 15 deletions.
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
```

### Prometheus
1. Install the Prometheus PHP client: `composer require promphp/prometheus_client_php`
2. Configuring the backend to use Prometheus, makes sense only if you have an endpoint to expose them.
Its purpose is only to format the registered metrics in a way that Prometheus can scrape them.

### NullDriver (for development)

If you need to disable metrics just set the backend to null:
Expand All @@ -102,6 +107,10 @@ METRICS_BACKEND=null

This `null` driver will simply discard any metrics.

```
METRICS_BACKEND=prometheus
```

## Sending an individual metric

You can create metric by using the facade like this:
Expand All @@ -121,15 +130,18 @@ The only required attribute is the `name`, everything else is optional.

This is how we are mapping metric attributes in our backends.

| Metric attribute | PostHog | InfluxDB | CloudWatch |
|------------------|-------------------|---------------|-------------------|
| name | event | measurement | MetricName |
| value | properties[value] | fields[value] | Value |
| unit | _ignored_ | _ignored_ | Unit |
| resolution | _ignored_ | _ignored_ | StorageResolution |
| tags | _ignored_ | tags | Dimensions |
| extra | properties | fields | _ignored_ |
| timestamp | _ignored_ | timestamp | Timestamp |
| Metric attribute | PostHog | InfluxDB | CloudWatch | Prometheus |
|------------------|-------------------|---------------|-------------------|-----------------------------------------------|
| name | event | measurement | MetricName | name |
| value | properties[value] | fields[value] | Value | value |
| unit | _ignored_ | _ignored_ | Unit | _ignored_ |
| resolution | _ignored_ | _ignored_ | StorageResolution | _ignored_ |
| tags | _ignored_ | tags | Dimensions | keys -> labelNames<br/> values -> labelValues |
| extra | properties | fields | _ignored_ | _ignored_ |
| timestamp | _ignored_ | timestamp | Timestamp | _ignored_ |
| description | _ignored_ | _ignored_ | _ignored_ | help |
| namespace | _ignored_ | _ignored_ | _ignored_ | namespace |
| type | _ignored_ | _ignored_ | _ignored_ | used to register counter or gauge metric |

See the [CloudWatch docs](http://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html)
and [InfluxDB docs](https://docs.influxdata.com/influxdb/latest/concepts/key_concepts/) for more information on their
Expand Down
8 changes: 3 additions & 5 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,22 @@
}
],
"require": {
"php": "^8.0",
"php": "^8.1",
"illuminate/support": "^5.6|^6.0|^7.0|^8.0|^9.0|^10.0"
},
"require-dev": {
"phpunit/phpunit": "^10.0",
"orchestra/testbench": "^8.0",
"mockery/mockery": "^1.6",
"aws/aws-sdk-php": "^3.2",
"influxdb/influxdb-php": "^1.15",
"posthog/posthog-php": "^3.0",
"influxdata/influxdb-client-php": "^3.4",
"guzzlehttp/guzzle": "^7.5"
},
"suggest": {
"posthog/posthog-php": "PostHog integration",
"aws/aws-sdk-php": "AWS CloudWatch integration",
"influxdb/influxdb-php": "For integration with InfluxDB 1.7 or earlier",
"influxdata/influxdb-client-php": "For integration with InfluxDB 1.8 or later"
"influxdata/influxdb-client-php": "For integration with InfluxDB 1.8 or later",
"promphp/prometheus_client_php": "For integration with Prometheus"
},
"autoload": {
"psr-4": {
Expand Down
8 changes: 8 additions & 0 deletions src/Drivers/AbstractDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ public function setExtra(array $extra): static
return $this;
}

/**
* Implement this, when the driver needs to expose metrics to be polled by a third party service such as prometheus
*/
public function formatted(): mixed
{
return null;
}

abstract public function format(Metric $metric);

abstract public function flush(): static;
Expand Down
79 changes: 79 additions & 0 deletions src/Drivers/PrometheusDriver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace STS\Metrics\Drivers;

use Illuminate\Support\Str;
use Prometheus\Collector;
use Prometheus\CollectorRegistry;
use Prometheus\Exception\MetricsRegistrationException;
use Prometheus\RendererInterface;
use STS\Metrics\Metric;
use STS\Metrics\MetricsServiceProvider;
use STS\Metrics\MetricType;
use UnhandledMatchError;

/**
* The idea of this driver is to use Prometheus\CollectorRegistry only to format the already collected metrics in prometheus format.
*/
class PrometheusDriver extends AbstractDriver
{
public function __construct(readonly private RendererInterface $renderer, readonly private CollectorRegistry $registry)
{

}

/**
* @throws MetricsRegistrationException
* @throws UnhandledMatchError
*/
public function format(Metric $metric): Collector
{
$namespace = Str::snake($metric->getNamespace());
$name = Str::snake($metric->getName());
$labelKeys = array_map(fn ($tag) => Str::snake($tag) , array_keys($metric->getTags()));
return match ($metric->getType()) {
MetricType::COUNTER => (function () use ($namespace, $name, $labelKeys, $metric) {
$counter = $this->registry->getOrRegisterCounter($namespace, $name, $metric->getDescription() ?? '', $labelKeys);
$counter->incBy($metric->getValue(), array_values($metric->getTags()));
return $counter;
})(),
MetricType::GAUGE => (function () use ($namespace, $name, $labelKeys, $metric) {
$gauge = $this->registry->getOrRegisterGauge($namespace, $name, $metric->getDescription() ?? '', $labelKeys);
$gauge->set($metric->getValue(), array_values($metric->getTags()));
return $gauge;
})(),
default => throw new UnhandledMatchError($metric->getType()),
};
}

public function flush(): static
{
$this->metrics = [];
$this->registry->wipeStorage();
return $this;
}

/**
* Renders all collected metrics in prometheus format.
* The result can be directly exposed on HTTP endpoint, for polling by Prometheus.
*
* If execution is thrown no matter in the context of long-running process or http request,
* there are handlers in @see MetricsServiceProvider to call flush and clear the state
*
* @return string
* @throws MetricsRegistrationException
* @throws UnhandledMatchError
*/
public function formatted(): string
{
// Always before formatting all metrics we need to wipe the registry storage and to register the metrics again.
// If we don't, we will increment existing counters instead of replacing them.
$this->registry->wipeStorage();

collect($this->getMetrics())->each(function (Metric $metric) {
$this->format($metric);
});

return $this->renderer->render($this->registry->getMetricFamilySamples());
}
}
45 changes: 45 additions & 0 deletions src/Metric.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ class Metric
*/
protected $resolution;

protected string $namespace = 'app';

protected ?MetricType $type = null;

protected ?string $description = null;

/**
* Metric constructor.
*
Expand Down Expand Up @@ -188,8 +194,47 @@ public function setDriver(AbstractDriver $driver): static
return $this;
}

public function setNamespace(string $namespace): void
{
$this->namespace = $namespace;
}

public function getNamespace(): string
{
return $this->namespace;
}

public function format(): mixed
{
return $this->getDriver()->format($this);
}

public function formatted(): mixed
{
return $this->getDriver()->formatted();
}

public function getType(): ?MetricType
{
return $this->type;
}

public function setType(?MetricType $type): static
{
$this->type = $type;

return $this;
}

public function getDescription(): ?string
{
return $this->description;
}

public function setDescription(?string $description = null): static
{
$this->description = $description;

return $this;
}
}
10 changes: 10 additions & 0 deletions src/MetricType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace STS\Metrics;

enum MetricType {
case COUNTER;
case SUMMARY;
case GAUGE;
case HISTOGRAM;
}
6 changes: 6 additions & 0 deletions src/MetricsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use STS\Metrics\Drivers\LogDriver;
use STS\Metrics\Drivers\NullDriver;
use STS\Metrics\Drivers\PostHog;
use STS\Metrics\Drivers\PrometheusDriver;

/**
* @mixin AbstractDriver
Expand Down Expand Up @@ -66,4 +67,9 @@ public function createNullDriver(): NullDriver
{
return new NullDriver();
}

public function createPrometheusDriver(): PrometheusDriver
{
return $this->container->make(PrometheusDriver::class);
}
}
13 changes: 13 additions & 0 deletions src/MetricsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use InfluxDB\Client;
use Prometheus\CollectorRegistry;
use Prometheus\RenderTextFormat;
use Prometheus\Storage\InMemory;
use STS\Metrics\Contracts\ShouldReportMetric;
use STS\Metrics\Drivers\CloudWatch;
use STS\Metrics\Drivers\InfluxDB;
Expand All @@ -17,6 +20,7 @@
use STS\Metrics\Adapters\InfluxDB1Adapter;
use STS\Metrics\Adapters\InfluxDB2Adapter;
use STS\Metrics\Drivers\PostHog;
use STS\Metrics\Drivers\PrometheusDriver;
use STS\Metrics\Octane\Listeners\FlushMetrics;

/**
Expand Down Expand Up @@ -51,6 +55,10 @@ public function register()
$this->app->singleton(PostHog::class, function () {
return $this->createPostHogDriver($this->app['config']['metrics.backends.posthog']);
});

$this->app->singleton(PrometheusDriver::class, function () {
return $this->createPrometheusDriver($this->app['config']['metrics.backends.prometheus']);
});
}

/**
Expand Down Expand Up @@ -236,4 +244,9 @@ protected function createPostHogDriver(array $config)
}
);
}

protected function createPrometheusDriver()
{
return new PrometheusDriver(new RenderTextFormat(), new CollectorRegistry(new InMemory(), false));
}
}
19 changes: 18 additions & 1 deletion src/Traits/ProvidesMetric.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Support\Str;
use STS\Metrics\Metric;
use STS\Metrics\MetricType;

trait ProvidesMetric
{
Expand All @@ -13,12 +14,14 @@ trait ProvidesMetric
public function createMetric(): Metric
{
return (new Metric($this->getMetricName()))
->setType($this->getMetricType())
->setValue($this->getMetricValue())
->setUnit($this->getMetricUnit())
->setTags($this->getMetricTags())
->setExtra($this->getMetricExtra())
->setTimestamp($this->getMetricTimestamp())
->setResolution($this->getMetricResolution());
->setResolution($this->getMetricResolution())
->setDescription($this->getMetricDescription());
}

public function getMetricName(): string
Expand Down Expand Up @@ -69,4 +72,18 @@ public function getMetricResolution(): int|null
? $this->metricResolution
: null;
}

public function getMetricType(): ?MetricType
{
return property_exists($this, 'metricType')
? $this->metricType
: null;
}

public function getMetricDescription(): ?string
{
return property_exists($this, 'metricDescription')
? $this->metricDescription
: null;
}
}
Loading

0 comments on commit fe1b99d

Please sign in to comment.