Skip to content

Commit 74274de

Browse files
committed
Implement Prometheus Counter type
1 parent 24ffe7e commit 74274de

13 files changed

+286
-0
lines changed

src/MetricTypes/Counter.php

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
namespace Spatie\Prometheus\MetricTypes;
4+
5+
use Closure;
6+
use Illuminate\Support\Arr;
7+
use Illuminate\Support\Str;
8+
use Prometheus\CollectorRegistry;
9+
use Prometheus\Counter as PrometheusCounter;
10+
11+
class Counter implements MetricType
12+
{
13+
protected array $values = [];
14+
15+
public function __construct(
16+
protected string $label,
17+
null|float|Closure|array $initialValue,
18+
protected ?string $name = null,
19+
protected ?string $namespace = null,
20+
protected ?string $helpText = null,
21+
protected ?array $labelNames = [],
22+
protected string $urlName = 'default',
23+
) {
24+
$this->name = $name ?? Str::slug($this->label, '_');
25+
26+
if (! is_null($initialValue)) {
27+
$this->setInitialValue($initialValue);
28+
}
29+
30+
$this->namespace = Str::of(config('prometheus.default_namespace'))
31+
->slug('_')
32+
->lower();
33+
}
34+
35+
public function namespace(string $namespace): self
36+
{
37+
$this->namespace = $namespace;
38+
39+
return $this;
40+
}
41+
42+
public function urlName(string $urlName): self
43+
{
44+
$this->urlName = $urlName;
45+
46+
return $this;
47+
}
48+
49+
public function getUrlName(): string
50+
{
51+
return $this->urlName;
52+
}
53+
54+
public function label(string $label): self
55+
{
56+
$this->labelNames[] = $label;
57+
58+
return $this;
59+
}
60+
61+
public function labels(array $labelNames): self
62+
{
63+
$this->labelNames = $labelNames;
64+
65+
return $this;
66+
}
67+
68+
public function name(string $name): self
69+
{
70+
$this->name = $name;
71+
72+
return $this;
73+
}
74+
75+
public function helpText(string $helpText): self
76+
{
77+
$this->helpText = $helpText;
78+
79+
return $this;
80+
}
81+
82+
public function setInitialValue(array|float|Closure $value, array|string $labelValues = []): self
83+
{
84+
$labelValues = Arr::wrap($labelValues);
85+
86+
$this->values[] = [$value, $labelValues];
87+
88+
return $this;
89+
}
90+
91+
public function inc(array|float|Closure $value = 1, array|string $labelValues = []): self
92+
{
93+
$labelValues = Arr::wrap($labelValues);
94+
95+
$this->values[] = [$value, $labelValues];
96+
97+
return $this;
98+
}
99+
100+
public function register(CollectorRegistry $registry): self
101+
{
102+
$counter = $registry->getOrRegisterCounter(
103+
namespace: $this->namespace,
104+
name: $this->name,
105+
help: $this->helpText ?? '',
106+
labels: $this->labelNames,
107+
);
108+
109+
collect($this->values)
110+
->each(function (array $valueAndLabels) use ($counter) {
111+
$this->handleValueAndLabels(counter: $counter, valueAndLabels: $valueAndLabels);
112+
});
113+
114+
return $this;
115+
}
116+
117+
protected function handleValueAndLabels(PrometheusCounter $counter, array $valueAndLabels)
118+
{
119+
[$value, $labels] = $valueAndLabels;
120+
$value = value($value);
121+
122+
if (is_array($value) && Arr::exists($value, 0) && is_array($value[0])) {
123+
foreach ($value as $valueAndLabels) {
124+
$this->handleValueAndLabels(counter: $counter, valueAndLabels: $valueAndLabels);
125+
}
126+
127+
return;
128+
}
129+
130+
if (is_array($value) && count($value) === 0) {
131+
return;
132+
}
133+
134+
if (is_array(($value))) {
135+
[$value, $labels] = $value;
136+
}
137+
138+
$counter->incBy(count: $value, labels: $labels);
139+
}
140+
}

src/Prometheus.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Spatie\Prometheus;
44

55
use Spatie\Prometheus\Collectors\Collector;
6+
use Spatie\Prometheus\MetricTypes\Counter;
67
use Spatie\Prometheus\MetricTypes\Gauge;
78
use Spatie\Prometheus\MetricTypes\MetricType;
89

@@ -25,6 +26,20 @@ public function addGauge(
2526
return $collector;
2627
}
2728

29+
public function addCounter(
30+
string $label,
31+
int|callable|null $initialValue = null,
32+
?string $name = null,
33+
?string $namespace = null,
34+
?string $helpText = null,
35+
): Counter {
36+
$collector = new Counter(label: $label, initialValue: $initialValue, name: $name, namespace: $namespace, helpText: $helpText);
37+
38+
$this->registerCollector($collector);
39+
40+
return $collector;
41+
}
42+
2843
public function registerCollector(MetricType $collector): self
2944
{
3045
$this->collectors[] = $collector;

tests/Http/Controllers/PrometheusMetricsControllerTest.php

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,104 @@
124124

125125
assertPrometheusResultsMatchesSnapshot();
126126
});
127+
128+
it('can render a simple counter with initial value', function () {
129+
/** @var \Spatie\Prometheus\MetricTypes\Counter $counter */
130+
Prometheus::addCounter('my counter');
131+
132+
assertPrometheusResultsMatchesSnapshot();
133+
});
134+
135+
it('can render a counter with all options', function () {
136+
/** @var \Spatie\Prometheus\MetricTypes\Counter $counter */
137+
Prometheus::addCounter('my counter')
138+
->namespace('other_namespace')
139+
->helpText('This is the help text')
140+
->name('alternative_name')
141+
->setInitialValue(123);
142+
143+
assertPrometheusResultsMatchesSnapshot();
144+
});
145+
146+
it('can increment a counter by one as default', function () {
147+
/** @var \Spatie\Prometheus\MetricTypes\Counter $counter */
148+
Prometheus::addCounter('my counter')
149+
->setInitialValue(1)
150+
->inc();
151+
152+
assertPrometheusResultsMatchesSnapshot();
153+
});
154+
155+
it('can increment a counter by a custom value', function () {
156+
/** @var \Spatie\Prometheus\MetricTypes\Counter $counter */
157+
$counter = Prometheus::addCounter('my counter');
158+
$counter->inc(2);
159+
160+
assertPrometheusResultsMatchesSnapshot();
161+
});
162+
163+
it('can render a counter that returns a single result with labels in the closure', closure: function () {
164+
Prometheus::addCounter('my counter', function () {
165+
return [1, ['label_value']];
166+
})->label('label_name');
167+
168+
assertPrometheusResultsMatchesSnapshot();
169+
});
170+
171+
it('can render a counter that returns a multiple results with labels in the closure', closure: function () {
172+
Prometheus::addCounter('my counter', function () {
173+
return [
174+
[123, ['label_value']],
175+
[456, ['label_value_2']],
176+
];
177+
})->label('label_name');
178+
179+
assertPrometheusResultsMatchesSnapshot();
180+
});
181+
182+
it('can render a counter with labels', function () {
183+
/** @var \Spatie\Prometheus\MetricTypes\Gauge $counter */
184+
$counter = Prometheus::addCounter('my counter');
185+
186+
$counter
187+
->namespace('other_namespace')
188+
->labels(['label_name_1', 'label_name_2'])
189+
->setInitialValue(123, ['label_value_1', 'label_value_2'])
190+
->setInitialValue(124, ['label_value_3', 'label_value_4']);
191+
192+
assertPrometheusResultsMatchesSnapshot();
193+
});
194+
195+
it('will render the counters on the default url', closure: function () {
196+
config()->set('prometheus.urls', [
197+
'default' => '/prometheus',
198+
'alternative' => '/my-alternative-route',
199+
]);
200+
201+
$this->reloadServiceProvider();
202+
203+
Prometheus::addCounter('my default counter', 123);
204+
Prometheus::addCounter('my alternative counter', 123)->urlName('alternative');
205+
206+
assertPrometheusResultsMatchesSnapshot();
207+
});
208+
209+
it('will render the counters on the alternative url', closure: function () {
210+
config()->set('prometheus.urls', [
211+
'default' => '/prometheus',
212+
'alternative' => '/my-alternative-route',
213+
]);
214+
215+
$this->reloadServiceProvider();
216+
217+
Prometheus::addCounter('my default counter', 123);
218+
Prometheus::addCounter('my alternative counter', 123)->urlName('alternative');
219+
220+
assertPrometheusResultsMatchesSnapshot('alternative');
221+
});
222+
223+
it('will convert the counter name to snake case', closure: function () {
224+
Prometheus::addCounter('My Counter', 123);
225+
226+
assertPrometheusResultsMatchesSnapshot();
227+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# HELP app_my_counter
2+
# TYPE app_my_counter counter
3+
app_my_counter 2
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# HELP app_my_counter
2+
# TYPE app_my_counter counter
3+
app_my_counter 2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# HELP app_my_counter
2+
# TYPE app_my_counter counter
3+
app_my_counter{label_name="label_value"} 123
4+
app_my_counter{label_name="label_value_2"} 456
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# HELP app_my_counter
2+
# TYPE app_my_counter counter
3+
app_my_counter{label_name="label_value"} 1
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# HELP other_namespace_alternative_name This is the help text
2+
# TYPE other_namespace_alternative_name counter
3+
other_namespace_alternative_name 123
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# HELP other_namespace_my_counter
2+
# TYPE other_namespace_my_counter counter
3+
other_namespace_my_counter{label_name_1="label_value_1",label_name_2="label_value_2"} 123
4+
other_namespace_my_counter{label_name_1="label_value_3",label_name_2="label_value_4"} 124
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)