Skip to content

Commit ad2a85f

Browse files
joec4iLKaemmerling
andauthored
[RenderTextFormat] Allow value errors to be rendered as comments (#142)
* [RenderTextFormat] Allow value errors to be rendered as comments. Redis and RedisNg allow samples with mismatching labels to be stored, which could cause ValueError to be thrown when rendering. Rendering would fail as a result, which is not ideal. - Change render() to accept an optional $silent parameter. When set to true, render the errors as comments instead of throwing them and failing the whole operation. Signed-off-by: Joe Cai <joe.cai@bigcommerce.com> --------- Signed-off-by: Joe Cai <joe.cai@bigcommerce.com> Co-authored-by: Lukas Kämmerling <github@lukas-kaemmerling.de>
1 parent c7c174b commit ad2a85f

File tree

2 files changed

+72
-2
lines changed

2 files changed

+72
-2
lines changed

src/Prometheus/RenderTextFormat.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,18 @@
55
namespace Prometheus;
66

77
use RuntimeException;
8+
use Throwable;
89

910
class RenderTextFormat implements RendererInterface
1011
{
1112
const MIME_TYPE = 'text/plain; version=0.0.4';
1213

1314
/**
1415
* @param MetricFamilySamples[] $metrics
16+
* @param bool $silent If true, render value errors as comments instead of throwing them.
1517
* @return string
1618
*/
17-
public function render(array $metrics): string
19+
public function render(array $metrics, bool $silent = false): string
1820
{
1921
usort($metrics, function (MetricFamilySamples $a, MetricFamilySamples $b): int {
2022
return strcmp($a->getName(), $b->getName());
@@ -25,7 +27,20 @@ public function render(array $metrics): string
2527
$lines[] = "# HELP " . $metric->getName() . " {$metric->getHelp()}";
2628
$lines[] = "# TYPE " . $metric->getName() . " {$metric->getType()}";
2729
foreach ($metric->getSamples() as $sample) {
28-
$lines[] = $this->renderSample($metric, $sample);
30+
try {
31+
$lines[] = $this->renderSample($metric, $sample);
32+
} catch (Throwable $e) {
33+
// Redis and RedisNg allow samples with mismatching labels to be stored, which could cause ValueError
34+
// to be thrown when rendering. If this happens, users can decide whether to ignore the error or not.
35+
// These errors will normally disappear after the storage is flushed.
36+
if (!$silent) {
37+
throw $e;
38+
}
39+
40+
$lines[] = "# Error: {$e->getMessage()}";
41+
$lines[] = "# Labels: " . json_encode(array_merge($metric->getLabelNames(), $sample->getLabelNames()));
42+
$lines[] = "# Values: " . json_encode(array_merge($sample->getLabelValues()));
43+
}
2944
}
3045
}
3146
return implode("\n", $lines) . "\n";

tests/Test/Prometheus/RenderTextFormatTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use Prometheus\RenderTextFormat;
1111
use PHPUnit\Framework\TestCase;
1212
use Prometheus\Storage\InMemory;
13+
use Prometheus\Storage\Redis;
14+
use ValueError;
1315

1416
class RenderTextFormatTest extends TestCase
1517
{
@@ -70,4 +72,57 @@ private function getExpectedOutput(): string
7072
7173
TEXTPLAIN;
7274
}
75+
76+
public function testValueErrorThrownWithInvalidSamples(): void
77+
{
78+
$namespace = 'foo';
79+
$counter = 'bar';
80+
$storage = new Redis(['host' => REDIS_HOST]);
81+
$storage->wipeStorage();
82+
83+
$registry = new CollectorRegistry($storage, false);
84+
$registry->registerCounter($namespace, $counter, 'counter-help-text', ['label1', 'label2'])
85+
->inc(['bob', 'alice']);
86+
87+
// Reload the registry with an updated counter config
88+
$registry = new CollectorRegistry($storage, false);
89+
$registry->registerCounter($namespace, $counter, 'counter-help-text', ['label1', 'label2', 'label3'])
90+
->inc(['bob', 'alice', 'eve']);
91+
92+
$this->expectException(ValueError::class);
93+
94+
$renderer = new RenderTextFormat();
95+
$renderer->render($registry->getMetricFamilySamples());
96+
}
97+
98+
public function testOutputWithInvalidSamplesSkipped(): void
99+
{
100+
$namespace = 'foo';
101+
$counter = 'bar';
102+
$storage = new Redis(['host' => REDIS_HOST]);
103+
$storage->wipeStorage();
104+
105+
$registry = new CollectorRegistry($storage, false);
106+
$registry->registerCounter($namespace, $counter, 'counter-help-text', ['label1', 'label2'])
107+
->inc(['bob', 'alice']);
108+
109+
// Reload the registry with an updated counter config
110+
$registry = new CollectorRegistry($storage, false);
111+
$registry->registerCounter($namespace, $counter, 'counter-help-text', ['label1', 'label2', 'label3'])
112+
->inc(['bob', 'alice', 'eve']);
113+
114+
$expectedOutput = '
115+
# HELP foo_bar counter-help-text
116+
# TYPE foo_bar counter
117+
foo_bar{label1="bob",label2="alice"} 1
118+
# Error: array_combine(): Argument #1 ($keys) and argument #2 ($values) must have the same number of elements
119+
# Labels: ["label1","label2"]
120+
# Values: ["bob","alice","eve"]
121+
';
122+
123+
$renderer = new RenderTextFormat();
124+
$output = $renderer->render($registry->getMetricFamilySamples(), true);
125+
126+
self::assertSame(trim($expectedOutput), trim($output));
127+
}
73128
}

0 commit comments

Comments
 (0)