Skip to content
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

2.0.0 #10

Merged
merged 19 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
/phpstan.neon export-ignore
/.releaserc.json export-ignore
/infection.json5 export-ignore
/docs/ export-ignore
101 changes: 76 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,6 @@ This package provides you an ability to collect and export [Prometheus](https://
* Won't break your business logic even if something is wrong with Metrics Storage
* Ready to use with static analysis tools (PHPStan, Psalm)

## Supported metric types

1. [Counter](https://prometheus.io/docs/concepts/metric_types/#counter)
2. [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge)
3. [Histogram](https://prometheus.io/docs/concepts/metric_types/#histogram)

Summary is still in development. [What can I do if my client library does not support the metric type I need?](https://prometheus.io/docs/practices/histograms/#what-can-i-do-if-my-client-library-does-not-support-the-metric-type-i-need)

## Adapters
* For Laravel: [zlodes/prometheus-client-laravel](https://github.com/zlodes/php-prometheus-client-laravel)

Expand All @@ -29,15 +21,31 @@ Summary is still in development. [What can I do if my client library does not su
composer require zlodes/prometheus-client
```

## Class responsibilities
## Flow

TL;DR: Read [Simple example](#simple-example).

### 1. Preparation

1. Set up a storage to store metrics. There are four interfaces can be implemented:
1. [CounterStorage](./src/Storage/Contracts/CounterStorage.php)
2. [GaugeStorage](./src/Storage/Contracts/GaugeStorage.php)
3. [HistogramStorage](./src/Storage/Contracts/HistogramStorage.php)
4. [SummaryStorage](./src/Storage/Contracts/SummaryStorage.php)
2. Set up a [Registry](./src/Registry/Registry.php) to register your metrics. [ArrayRegistry](./src/Registry/ArrayRegistry.php) is a default implementation.
3. Register your metrics using the Registry from step 2.

### 2. Collecting

| Interface | Description | Default implementation |
|---------------------------------------|------------------------------|-----------------------------------------------------------------|
| [Registry](src/Registry/Registry.php) | To declare a specific metric | [ArrayRegistry](src/Registry/ArrayRegistry.php) |
| [Storage](src/Storage/Storage.php) | Metrics values storage | [InMemoryStorage](src/Storage/InMemoryStorage.php) |
| [Exporter](src/Exporter/Exporter.php) | Output collected metrics | [StoredMetricsExporter](src/Exporter/StoredMetricsExporter.php) |
1. Get a collector for your metric from a [CollectorFactory](./src/Collector/CollectorFactory.php)
2. Call metric update method (e.g. `increment` on CounterCollector)

Each class should be registered as a service. As a `singleton` in Laravel or `shared` service in Symfony.
### 3. Exporting

1. Create a controller to export metrics. Your controller should use [Exporter](./src/Exporter/Exporter.php). [FetcherExporter](./src/Exporter/FetcherExporter.php) is a default implementation.
2. Set up a Prometheus to scrape metrics from your application using the controller from step 1.

![](./docs/export.png)

## Simple example

Expand All @@ -46,15 +54,19 @@ Each class should be registered as a service. As a `singleton` in Laravel or `sh

use Psr\Log\NullLogger;
use Zlodes\PrometheusClient\Collector\CollectorFactory;
use Zlodes\PrometheusClient\Exporter\StoredMetricsExporter;
use Zlodes\PrometheusClient\Exporter\FetcherExporter;
use Zlodes\PrometheusClient\Metric\Counter;
use Zlodes\PrometheusClient\Metric\Gauge;
use Zlodes\PrometheusClient\Metric\Histogram;
use Zlodes\PrometheusClient\Registry\ArrayRegistry;
use Zlodes\PrometheusClient\Storage\InMemoryStorage;

$registry = new ArrayRegistry();
$storage = new InMemoryStorage();

$counterStorage = new InMemoryCounterStorage();
$gaugeStorage = new InMemoryGaugeStorage();
$histogramStorage = new InMemoryHistogramStorage();
$summaryStorage = new InMemorySummaryStorage();

// Register your metrics
$registry
Expand All @@ -65,13 +77,22 @@ $registry
new Counter('steps', 'Steps count')
)
->registerMetric(
new Histogram('request_duration', 'Request duration in seconds'),
(new Histogram('http_request_duration_seconds', 'HTTP Request duration'))
->withBuckets([0.1, 0.5, 1]),
)
->registerMetric(
(new Summary('memory_used', 'Used memory in bytes'))
->withQuantiles([0.5, 0.9, 0.99])
);

// Create a Collector factory

$collectorFactory = new CollectorFactory(
$registry,
$storage,
$counterStorage,
$gaugeStorage,
$histogramStorage,
$summaryStorage,
new NullLogger(),
);

Expand All @@ -91,34 +112,64 @@ $collectorFactory
->increment();

$requestTimer = $collectorFactory
->histogram('request_duration')
->histogram('http_request_duration_seconds')
->startTimer();

usleep(50_000);

$requestTimer->stop();

$collectorFactory
->summary('memory_used')
->update(100);

$collectorFactory
->summary('memory_used')
->update(200);

// Export metrics
$exporter = new StoredMetricsExporter(
$fetcher = new StoredMetricsFetcher(
$registry,
$storage,
$counterStorage,
$gaugeStorage,
$histogramStorage,
$summaryStorage,
);

$exporter = new FetcherExporter($fetcher);

foreach ($exporter->export() as $metricOutput) {
echo $metricOutput . "\n\n";
}
```

Output example:
```
# HELP steps Steps count
# TYPE steps counter
steps 1

# HELP body_temperature Body temperature in Celsius
# TYPE body_temperature gauge
body_temperature{source="armpit"} 36.6
body_temperature{source="ass"} 37.2

# HELP steps Steps count
# TYPE steps counter
steps 1
# HELP http_request_duration_seconds HTTP Request duration
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds{le="0.1"} 1
http_request_duration_seconds{le="0.5"} 1
http_request_duration_seconds{le="1"} 1
http_request_duration_seconds{le="+Inf"} 1
http_request_duration_seconds_sum 0.050071506
http_request_duration_seconds_count 1

# HELP memory_used Used memory in bytes
# TYPE memory_used summary
memory_used{quantile="0.5"} 150
memory_used{quantile="0.9"} 190
memory_used{quantile="0.99"} 199
memory_used_sum 300
memory_used_count 2
```

## Testing
Expand Down
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@
},
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true,
"infection/extension-installer": true
"ergebnis/composer-normalize": true,
"infection/extension-installer": true,
"phpstan/extension-installer": true
},
"sort-packages": true
},
Expand All @@ -63,7 +63,7 @@
"phpmd": "./vendor/bin/phpmd src text phpmd.xml",
"phpstan": "./vendor/bin/phpstan",
"psalm": "./vendor/bin/psalm --show-info=true",
"test": "./vendor/bin/phpunit",
"test": "./vendor/bin/phpunit --display-warnings",
"test:coverage": [
"@putenv XDEBUG_MODE=coverage",
"@test"
Expand Down
Binary file added docs/export.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions docs/export.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@startuml
'https://plantuml.com/component-diagram

node "Userland code" {
HTTP - [Metrics Controller]

[Configuration]
}

node "zlodes/php-prometheus-client" {
[Fetcher]
[Exporter]
[Registry]

package "Storages" {
[CounterStorage]
[GaugeStorage]
[HistogramStorage]
[SummaryStorage]
}
}

[Metrics Controller] --> [Exporter]: Get output strings iterator for exporter
[Exporter] -> [Fetcher]: Get metrics with values
[Fetcher] -> [Registry]: Get metric definition
[Fetcher] --> [CounterStorage]: Get values
[Fetcher] --> [GaugeStorage]: Get values
[Fetcher] --> [HistogramStorage]: Get values
[Fetcher] --> [SummaryStorage]: Get values

[Configuration] --> [Registry]: Register a metric

@enduml
30 changes: 10 additions & 20 deletions src/Collector/ByType/CounterCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,24 @@

use Psr\Log\LoggerInterface;
use Webmozart\Assert\Assert;
use Zlodes\PrometheusClient\Collector\WithLabels;
use Zlodes\PrometheusClient\Collector\Collector;
use Zlodes\PrometheusClient\Exception\StorageWriteException;
use Zlodes\PrometheusClient\Metric\Counter;
use Zlodes\PrometheusClient\Storage\Commands\IncrementCounter;
use Zlodes\PrometheusClient\Storage\Contracts\CounterStorage;
use Zlodes\PrometheusClient\Storage\DTO\MetricNameWithLabels;
use Zlodes\PrometheusClient\Storage\DTO\MetricValue;
use Zlodes\PrometheusClient\Storage\Storage;

/**
* @final
*/
class CounterCollector
class CounterCollector extends Collector
{
use WithLabels;

/**
* @internal Zlodes\PrometheusClient\Collector
*/
public function __construct(
private readonly Counter $counter,
private readonly Storage $storage,
private readonly CounterStorage $storage,
private readonly LoggerInterface $logger,
) {
}
Expand All @@ -40,25 +38,17 @@ public function increment(int|float $value = 1): void
Assert::true($value > 0, 'Increment value of Counter metric MUST be positive');

$counter = $this->counter;
$labels = $this->composeLabels();
$labels = $this->getLabels();

try {
$this->storage->incrementValue(
new MetricValue(
new MetricNameWithLabels($counter->getName(), $labels),
$this->storage->incrementCounter(
new IncrementCounter(
new MetricNameWithLabels($counter->name, $labels),
$value
)
);
} catch (StorageWriteException $e) {
$this->logger->error("Cannot increment counter {$counter->getName()}: $e");
$this->logger->error("Cannot increment counter {$counter->name}: $e");
}
}

/**
* @return array<non-empty-string, non-empty-string>
*/
private function composeLabels(): array
{
return array_merge($this->counter->getInitialLabels(), $this->labels);
}
}
52 changes: 10 additions & 42 deletions src/Collector/ByType/GaugeCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,42 @@
namespace Zlodes\PrometheusClient\Collector\ByType;

use Psr\Log\LoggerInterface;
use Zlodes\PrometheusClient\Collector\WithLabels;
use Zlodes\PrometheusClient\Collector\Collector;
use Zlodes\PrometheusClient\Exception\StorageWriteException;
use Zlodes\PrometheusClient\Metric\Gauge;
use Zlodes\PrometheusClient\Storage\Commands\UpdateGauge;
use Zlodes\PrometheusClient\Storage\Contracts\GaugeStorage;
use Zlodes\PrometheusClient\Storage\DTO\MetricNameWithLabels;
use Zlodes\PrometheusClient\Storage\DTO\MetricValue;
use Zlodes\PrometheusClient\Storage\Storage;

/**
* @final
*/
class GaugeCollector
class GaugeCollector extends Collector
{
use WithLabels;

/**
* @internal Zlodes\PrometheusClient\Collector
*/
public function __construct(
private readonly Gauge $gauge,
private readonly Storage $storage,
private readonly GaugeStorage $storage,
private readonly LoggerInterface $logger,
) {
}

/**
* @param positive-int|float $value
*
* @return void
*/
public function increment(int|float $value = 1): void
{
$gauge = $this->gauge;
$labels = $this->composeLabels();

try {
$this->storage->incrementValue(
new MetricValue(
new MetricNameWithLabels($gauge->getName(), $labels),
$value
)
);
} catch (StorageWriteException $e) {
$this->logger->error("Cannot increment gauge {$gauge->getName()}: $e");
}
}

public function update(int|float $value): void
{
$gauge = $this->gauge;
$labels = $this->composeLabels();
$labels = $this->getLabels();

try {
$this->storage->setValue(
new MetricValue(
new MetricNameWithLabels($gauge->getName(), $labels),
$this->storage->updateGauge(
new UpdateGauge(
new MetricNameWithLabels($gauge->name, $labels),
$value
)
);
} catch (StorageWriteException $e) {
$this->logger->error("Cannot set value of gauge {$gauge->getName()}: $e");
$this->logger->error("Cannot set value of gauge {$gauge->name}: $e");
}
}

/**
* @return array<non-empty-string, non-empty-string>
*/
private function composeLabels(): array
{
return array_merge($this->gauge->getInitialLabels(), $this->labels);
}
}
Loading
Loading