Skip to content

Commit 62bbada

Browse files
authored
Merge pull request #6 from utopia-php/feat-observable-gauge
Add ObservableGauge metric type
2 parents 9997ebf + 5db0fd4 commit 62bbada

File tree

7 files changed

+199
-10
lines changed

7 files changed

+199
-10
lines changed

README.md

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,78 @@ Init in your application:
2222

2323
require_once __DIR__ . '/vendor/autoload.php';
2424

25-
// Create a Telemetry instance using OpenTelemetry adapter.
2625
use Utopia\Telemetry\Adapter\OpenTelemetry;
2726

28-
$telemetry = new OpenTelemetry('http://localhost:4138', 'namespace', 'app', 'unique-instance-name');
29-
$counter = $telemetry->createUpDownCounter('http.server.active_requests', '{request}');
27+
$telemetry = new OpenTelemetry('http://localhost:4318/v1/metrics', 'namespace', 'app', 'unique-instance-id');
3028

31-
$counter->add(1);
32-
$counter->add(2);
33-
34-
// Periodically collect metrics and send them to the configured OpenTelemetry endpoint.
29+
// Periodically collect and export metrics to the configured OpenTelemetry endpoint.
3530
$telemetry->collect();
3631

3732
// Example using Swoole
3833
\Swoole\Timer::tick(60_000, fn () => $telemetry->collect());
3934
```
4035

36+
## Metric Types
37+
38+
### Counter
39+
40+
A monotonically increasing counter. Only positive increments are allowed.
41+
42+
```php
43+
$counter = $telemetry->createCounter('http.server.requests', '{request}', 'Total HTTP requests');
44+
45+
$counter->add(1);
46+
$counter->add(1, ['method' => 'GET', 'status' => '200']);
47+
```
48+
49+
### UpDownCounter
50+
51+
A counter that can increase or decrease. Useful for tracking values like active connections.
52+
53+
```php
54+
$upDownCounter = $telemetry->createUpDownCounter('http.server.active_requests', '{request}', 'Active HTTP requests');
55+
56+
$upDownCounter->add(1); // request started
57+
$upDownCounter->add(-1); // request finished
58+
```
59+
60+
### Histogram
61+
62+
Records a distribution of values. Useful for measuring latency or payload sizes.
63+
64+
```php
65+
$histogram = $telemetry->createHistogram('http.server.request.duration', 'ms', 'HTTP request duration');
66+
67+
$histogram->record(142);
68+
$histogram->record(98.5, ['route' => '/api/users']);
69+
```
70+
71+
### Gauge
72+
73+
Records an instantaneous measurement. Useful for values that can arbitrarily go up or down.
74+
75+
```php
76+
$gauge = $telemetry->createGauge('system.memory.usage', 'By', 'Memory usage');
77+
78+
$gauge->record(1_073_741_824);
79+
$gauge->record(536_870_912, ['host' => 'server-1']);
80+
```
81+
82+
### ObservableGauge
83+
84+
An asynchronous gauge whose value is collected via a callback at export time. Useful for values that are expensive to compute or come from an external source (e.g. CPU usage, queue depth).
85+
86+
```php
87+
$observableGauge = $telemetry->createObservableGauge('process.cpu.usage', '%', 'CPU usage');
88+
89+
$observableGauge->observe(function (callable $observer): void {
90+
// This callback is invoked each time metrics are collected.
91+
$observer(sys_getloadavg()[0] * 100);
92+
$observer(72.4, ['core' => '0']);
93+
$observer(68.1, ['core' => '1']);
94+
});
95+
```
96+
4197
## System Requirements
4298

4399
Utopia Framework requires PHP 8.0 or later. We recommend using the latest PHP version whenever possible.

src/Telemetry/Adapter.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,15 @@ public function createUpDownCounter(
4444
array $advisory = [],
4545
): UpDownCounter;
4646

47+
/**
48+
* @param array<string, mixed> $advisory
49+
*/
50+
public function createObservableGauge(
51+
string $name,
52+
?string $unit = null,
53+
?string $description = null,
54+
array $advisory = [],
55+
): ObservableGauge;
56+
4757
public function collect(): bool;
4858
}

src/Telemetry/Adapter/None.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Utopia\Telemetry\Counter;
77
use Utopia\Telemetry\Gauge;
88
use Utopia\Telemetry\Histogram;
9+
use Utopia\Telemetry\ObservableGauge;
910
use Utopia\Telemetry\UpDownCounter;
1011

1112
class None implements Adapter
@@ -70,6 +71,18 @@ public function add(float|int $amount, iterable $attributes = []): void
7071
};
7172
}
7273

74+
/**
75+
* @param array<string, mixed> $advisory
76+
*/
77+
public function createObservableGauge(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): ObservableGauge
78+
{
79+
return new class () extends ObservableGauge {
80+
public function observe(callable $callback): void
81+
{
82+
}
83+
};
84+
}
85+
7386
public function collect(): bool
7487
{
7588
return true;

src/Telemetry/Adapter/OpenTelemetry.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use OpenTelemetry\API\Metrics\GaugeInterface;
77
use OpenTelemetry\API\Metrics\HistogramInterface;
88
use OpenTelemetry\API\Metrics\MeterInterface;
9+
use OpenTelemetry\API\Metrics\ObserverInterface;
910
use OpenTelemetry\API\Metrics\UpDownCounterInterface;
1011
use OpenTelemetry\Contrib\Otlp\ContentTypes;
1112
use OpenTelemetry\Contrib\Otlp\MetricExporter;
@@ -25,6 +26,7 @@
2526
use Utopia\Telemetry\Counter;
2627
use Utopia\Telemetry\Gauge;
2728
use Utopia\Telemetry\Histogram;
29+
use Utopia\Telemetry\ObservableGauge;
2830
use Utopia\Telemetry\UpDownCounter;
2931

3032
class OpenTelemetry implements Adapter
@@ -34,13 +36,14 @@ class OpenTelemetry implements Adapter
3436
private MeterInterface $meter;
3537

3638
/**
37-
* @var array<class-string, array<string, Counter|UpDownCounter|Histogram|Gauge>>
39+
* @var array<class-string, array<string, Counter|UpDownCounter|Histogram|Gauge|ObservableGauge>>
3840
*/
3941
private array $meterStorage = [
4042
Counter::class => [],
4143
UpDownCounter::class => [],
4244
Histogram::class => [],
4345
Gauge::class => [],
46+
ObservableGauge::class => [],
4447
];
4548

4649
/**
@@ -103,12 +106,12 @@ protected function createExporter(TransportInterface $transport): MetricExporter
103106
}
104107

105108
/**
106-
* @template T of Counter|UpDownCounter|Histogram|Gauge
109+
* @template T of Counter|UpDownCounter|Histogram|Gauge|ObservableGauge
107110
* @param class-string<T> $type
108111
* @param callable(): T $creator
109112
* @return T
110113
*/
111-
private function createMeter(string $type, string $name, callable $creator): Counter|UpDownCounter|Histogram|Gauge
114+
private function createMeter(string $type, string $name, callable $creator): Counter|UpDownCounter|Histogram|Gauge|ObservableGauge
112115
{
113116
if (! isset($this->meterStorage[$type][$name])) {
114117
$this->meterStorage[$type][$name] = $creator();
@@ -222,6 +225,39 @@ public function add(float|int $amount, iterable $attributes = []): void
222225
});
223226
}
224227

228+
/**
229+
* Create an ObservableGauge metric
230+
*
231+
* @param array<string, mixed> $advisory
232+
*/
233+
public function createObservableGauge(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): ObservableGauge
234+
{
235+
return $this->createMeter(ObservableGauge::class, $name, function () use ($name, $unit, $description, $advisory) {
236+
$otelGauge = $this->meter->createObservableGauge($name, $unit, $description, $advisory);
237+
238+
return new class ($otelGauge) extends ObservableGauge {
239+
private ?\Closure $callback = null;
240+
241+
public function __construct(private \OpenTelemetry\API\Metrics\ObservableGaugeInterface $gauge)
242+
{
243+
$this->gauge->observe(function (ObserverInterface $observer): void {
244+
if ($this->callback !== null) {
245+
($this->callback)(function (float|int $value, iterable $attributes = []) use ($observer): void {
246+
/** @var iterable<non-empty-string, array<mixed>|bool|float|int|string|null> $attributes */
247+
$observer->observe($value, $attributes);
248+
});
249+
}
250+
});
251+
}
252+
253+
public function observe(callable $callback): void
254+
{
255+
$this->callback = \Closure::fromCallable($callback);
256+
}
257+
};
258+
});
259+
}
260+
225261
/**
226262
* Collect and export metrics
227263
*/

src/Telemetry/Adapter/Test.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Utopia\Telemetry\Counter;
77
use Utopia\Telemetry\Gauge;
88
use Utopia\Telemetry\Histogram;
9+
use Utopia\Telemetry\ObservableGauge;
910
use Utopia\Telemetry\UpDownCounter;
1011

1112
/**
@@ -33,6 +34,11 @@ class Test implements Adapter
3334
*/
3435
public array $upDownCounters = [];
3536

37+
/**
38+
* @var array<string, ObservableGauge>
39+
*/
40+
public array $observableGauges = [];
41+
3642
/**
3743
* @param array<string, mixed> $advisory
3844
*/
@@ -125,6 +131,23 @@ public function add(float|int $amount, iterable $attributes = []): void
125131
return $upDownCounter;
126132
}
127133

134+
/**
135+
* @param array<string, mixed> $advisory
136+
*/
137+
public function createObservableGauge(string $name, ?string $unit = null, ?string $description = null, array $advisory = []): ObservableGauge
138+
{
139+
$gauge = new class () extends ObservableGauge {
140+
public ?\Closure $callback = null;
141+
142+
public function observe(callable $callback): void
143+
{
144+
$this->callback = \Closure::fromCallable($callback);
145+
}
146+
};
147+
$this->observableGauges[$name] = $gauge;
148+
return $gauge;
149+
}
150+
128151
public function collect(): bool
129152
{
130153
return true;

src/Telemetry/ObservableGauge.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Utopia\Telemetry;
4+
5+
abstract class ObservableGauge
6+
{
7+
/**
8+
* Register an observation callback that will be invoked during collection.
9+
*
10+
* The callback receives an observer callable that can be called with a value and optional attributes
11+
* to record an observation: $observer(float|int $value, iterable $attributes = [])
12+
*
13+
* @param callable(callable(float|int, iterable<non-empty-string, array<mixed>|bool|float|int|string|null>): void): void $callback
14+
*/
15+
abstract public function observe(callable $callback): void;
16+
}

tests/Telemetry/Adapter/OpenTelemetryTestCase.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Utopia\Telemetry\Counter;
99
use Utopia\Telemetry\Gauge;
1010
use Utopia\Telemetry\Histogram;
11+
use Utopia\Telemetry\ObservableGauge;
1112
use Utopia\Telemetry\UpDownCounter;
1213

1314
/**
@@ -159,6 +160,38 @@ public function testUpDownCounterAdd(): void
159160
$this->assertTrue(true);
160161
}
161162

163+
public function testCreateObservableGauge(): void
164+
{
165+
$gauge = $this->adapter->createObservableGauge(
166+
name: 'test_observable_gauge',
167+
unit: 'bytes',
168+
description: 'Test observable gauge metric'
169+
);
170+
171+
$this->assertInstanceOf(ObservableGauge::class, $gauge);
172+
}
173+
174+
public function testObservableGaugeObserve(): void
175+
{
176+
$gauge = $this->adapter->createObservableGauge('observe_test_gauge');
177+
178+
// Should not throw
179+
$gauge->observe(function (callable $observer): void {
180+
$observer(1024);
181+
$observer(2048.5, ['host' => 'server-1']);
182+
});
183+
184+
$this->assertTrue(true);
185+
}
186+
187+
public function testObservableGaugeCaching(): void
188+
{
189+
$gauge1 = $this->adapter->createObservableGauge('cached_observable_gauge');
190+
$gauge2 = $this->adapter->createObservableGauge('cached_observable_gauge');
191+
192+
$this->assertSame($gauge1, $gauge2);
193+
}
194+
162195
public function testMeterCaching(): void
163196
{
164197
$counter1 = $this->adapter->createCounter('cached_counter');
@@ -229,10 +262,12 @@ public function testNullOptionalParameters(): void
229262
$histogram = $this->adapter->createHistogram('minimal_histogram');
230263
$gauge = $this->adapter->createGauge('minimal_gauge');
231264
$upDownCounter = $this->adapter->createUpDownCounter('minimal_updown');
265+
$observableGauge = $this->adapter->createObservableGauge('minimal_observable_gauge');
232266

233267
$this->assertInstanceOf(Counter::class, $counter);
234268
$this->assertInstanceOf(Histogram::class, $histogram);
235269
$this->assertInstanceOf(Gauge::class, $gauge);
236270
$this->assertInstanceOf(UpDownCounter::class, $upDownCounter);
271+
$this->assertInstanceOf(ObservableGauge::class, $observableGauge);
237272
}
238273
}

0 commit comments

Comments
 (0)