Skip to content

Commit 9774381

Browse files
committed
feature: telemetry error handling
1 parent dc86e52 commit 9774381

61 files changed

Lines changed: 1921 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/DSL/functions.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Flow\Bridge\Monolog\Telemetry\{LogRecordConverter, SeverityMapper, TelemetryHandler, ValueNormalizer};
88
use Flow\ETL\Attribute\{DocumentationDSL, Module, Type as DSLType};
9+
use Flow\Telemetry\ErrorHandler\{ErrorHandler, ErrorLogHandler};
910
use Flow\Telemetry\Logger\{Logger, Severity};
1011
use Monolog\Level;
1112

@@ -146,6 +147,7 @@ function telemetry_handler(
146147
LogRecordConverter $converter = new LogRecordConverter(),
147148
Level $level = Level::Debug,
148149
bool $bubble = true,
150+
ErrorHandler $errorHandler = new ErrorLogHandler(),
149151
) : TelemetryHandler {
150-
return new TelemetryHandler($logger, $converter, $level, $bubble);
152+
return new TelemetryHandler($logger, $converter, $level, $bubble, $errorHandler);
151153
}

src/bridge/monolog/telemetry/src/Flow/Bridge/Monolog/Telemetry/TelemetryHandler.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flow\Bridge\Monolog\Telemetry;
66

7+
use Flow\Telemetry\ErrorHandler\{ErrorHandler, ErrorLogHandler};
78
use Flow\Telemetry\Logger\Logger;
89
use Monolog\Handler\AbstractProcessingHandler;
910
use Monolog\{Level, LogRecord};
@@ -43,12 +44,17 @@ public function __construct(
4344
private readonly LogRecordConverter $converter = new LogRecordConverter(),
4445
Level $level = Level::Debug,
4546
bool $bubble = true,
47+
private readonly ErrorHandler $errorHandler = new ErrorLogHandler(),
4648
) {
4749
parent::__construct($level, $bubble);
4850
}
4951

5052
protected function write(LogRecord $record) : void
5153
{
52-
$this->logger->emit($this->converter->convert($record));
54+
try {
55+
$this->logger->emit($this->converter->convert($record));
56+
} catch (\Throwable $e) {
57+
$this->errorHandler->handle($e);
58+
}
5359
}
5460
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Monolog\Telemetry\Tests\Unit;
6+
7+
use Flow\Bridge\Monolog\Telemetry\TelemetryHandler;
8+
use Flow\Telemetry\Context\MemoryContextStorage;
9+
use Flow\Telemetry\Logger\LoggerProvider;
10+
use Flow\Telemetry\Provider\Memory\MemoryLogProcessor;
11+
use Flow\Telemetry\Provider\Void\VoidExporter;
12+
use Flow\Telemetry\Resource;
13+
use Flow\Telemetry\Tests\Mother\ErrorHandlerSpy;
14+
use Monolog\{Level, Logger as MonologLogger};
15+
use PHPUnit\Framework\TestCase;
16+
use Psr\Clock\ClockInterface;
17+
18+
final class TelemetryHandlerErrorHandlingTest extends TestCase
19+
{
20+
public function test_does_not_propagate_exception_to_monolog_caller() : void
21+
{
22+
$this->expectNotToPerformAssertions();
23+
24+
$throwingClock = new class implements ClockInterface {
25+
public function now() : \DateTimeImmutable
26+
{
27+
throw new \RuntimeException('clock blew up');
28+
}
29+
};
30+
31+
$loggerProvider = new LoggerProvider(
32+
new MemoryLogProcessor(new VoidExporter()),
33+
$throwingClock,
34+
new MemoryContextStorage(),
35+
);
36+
37+
$logger = $loggerProvider->logger(Resource::empty(), 'test-scope');
38+
$spy = new ErrorHandlerSpy();
39+
$handler = new TelemetryHandler($logger, level: Level::Debug, errorHandler: $spy);
40+
41+
$monolog = new MonologLogger('test-channel');
42+
$monolog->pushHandler($handler);
43+
44+
$monolog->info('hello');
45+
}
46+
47+
public function test_routes_emit_failures_to_error_handler() : void
48+
{
49+
$throwingClock = new class implements ClockInterface {
50+
public function now() : \DateTimeImmutable
51+
{
52+
throw new \RuntimeException('clock blew up');
53+
}
54+
};
55+
56+
$loggerProvider = new LoggerProvider(
57+
new MemoryLogProcessor(new VoidExporter()),
58+
$throwingClock,
59+
new MemoryContextStorage(),
60+
);
61+
62+
$logger = $loggerProvider->logger(Resource::empty(), 'test-scope');
63+
$spy = new ErrorHandlerSpy();
64+
$handler = new TelemetryHandler($logger, errorHandler: $spy);
65+
66+
$monolog = new MonologLogger('test-channel');
67+
$monolog->pushHandler($handler);
68+
69+
$monolog->info('hello');
70+
71+
self::assertSame(1, $spy->count());
72+
self::assertSame('clock blew up', $spy->last()?->getMessage());
73+
}
74+
}

src/bridge/psr3/telemetry/src/Flow/Bridge/Psr3/Telemetry/DSL/functions.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Flow\Bridge\Psr3\Telemetry\{LogRecordConverter, SeverityMapper, TelemetryLogger, ValueNormalizer};
88
use Flow\ETL\Attribute\{DocumentationDSL, Module, Type as DSLType};
9+
use Flow\Telemetry\ErrorHandler\{ErrorHandler, ErrorLogHandler};
910
use Flow\Telemetry\Logger\{Logger, Severity};
1011

1112
/**
@@ -26,8 +27,9 @@
2627
function psr3_telemetry_logger(
2728
Logger $logger,
2829
LogRecordConverter $converter = new LogRecordConverter(),
30+
ErrorHandler $errorHandler = new ErrorLogHandler(),
2931
) : TelemetryLogger {
30-
return new TelemetryLogger($logger, $converter);
32+
return new TelemetryLogger($logger, $converter, $errorHandler);
3133
}
3234

3335
/**

src/bridge/psr3/telemetry/src/Flow/Bridge/Psr3/Telemetry/TelemetryLogger.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
namespace Flow\Bridge\Psr3\Telemetry;
66

77
use Flow\Bridge\Psr3\Telemetry\Exception\InvalidArgumentException;
8+
use Flow\Telemetry\ErrorHandler\{ErrorHandler, ErrorLogHandler};
89
use Flow\Telemetry\Logger\Logger;
9-
use Psr\Log\AbstractLogger;
10+
use Psr\Log\{AbstractLogger, InvalidArgumentException as PsrInvalidArgumentException};
1011

1112
final class TelemetryLogger extends AbstractLogger
1213
{
1314
public function __construct(
1415
private readonly Logger $logger,
1516
private readonly LogRecordConverter $converter = new LogRecordConverter(),
17+
private readonly ErrorHandler $errorHandler = new ErrorLogHandler(),
1618
) {
1719
}
1820

@@ -25,6 +27,12 @@ public function log($level, string|\Stringable $message, array $context = []) :
2527
throw new InvalidArgumentException('PSR-3 log level must be a string or Stringable.');
2628
}
2729

28-
$this->logger->emit($this->converter->convert($level, $message, $context));
30+
try {
31+
$this->logger->emit($this->converter->convert($level, $message, $context));
32+
} catch (PsrInvalidArgumentException $e) {
33+
throw $e;
34+
} catch (\Throwable $e) {
35+
$this->errorHandler->handle($e);
36+
}
2937
}
3038
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Flow\Bridge\Psr3\Telemetry\Tests\Unit;
6+
7+
use Flow\Bridge\Psr3\Telemetry\Exception\InvalidArgumentException;
8+
use Flow\Bridge\Psr3\Telemetry\TelemetryLogger;
9+
use Flow\Telemetry\Context\MemoryContextStorage;
10+
use Flow\Telemetry\Logger\LoggerProvider;
11+
use Flow\Telemetry\Provider\Clock\SystemClock;
12+
use Flow\Telemetry\Provider\Memory\MemoryLogProcessor;
13+
use Flow\Telemetry\Provider\Void\VoidExporter;
14+
use Flow\Telemetry\Resource;
15+
use Flow\Telemetry\Tests\Mother\ErrorHandlerSpy;
16+
use PHPUnit\Framework\TestCase;
17+
use Psr\Clock\ClockInterface;
18+
use Psr\Log\LogLevel;
19+
20+
final class TelemetryLoggerErrorHandlingTest extends TestCase
21+
{
22+
public function test_continues_emitting_after_normal_log_call() : void
23+
{
24+
$loggerProvider = new LoggerProvider(
25+
new MemoryLogProcessor(new VoidExporter()),
26+
new SystemClock(),
27+
new MemoryContextStorage(),
28+
);
29+
30+
$logger = $loggerProvider->logger(Resource::empty(), 'test-scope');
31+
$spy = new ErrorHandlerSpy();
32+
$psr3 = new TelemetryLogger($logger, errorHandler: $spy);
33+
34+
$psr3->log(LogLevel::INFO, 'hello');
35+
36+
self::assertSame(0, $spy->count());
37+
}
38+
39+
public function test_invalid_level_is_not_routed_to_error_handler() : void
40+
{
41+
$loggerProvider = new LoggerProvider(
42+
new MemoryLogProcessor(new VoidExporter()),
43+
new SystemClock(),
44+
new MemoryContextStorage(),
45+
);
46+
47+
$logger = $loggerProvider->logger(Resource::empty(), 'test-scope');
48+
$spy = new ErrorHandlerSpy();
49+
$psr3 = new TelemetryLogger($logger, errorHandler: $spy);
50+
51+
try {
52+
$psr3->log(new \stdClass(), 'message');
53+
} catch (InvalidArgumentException) {
54+
}
55+
56+
self::assertSame(0, $spy->count());
57+
}
58+
59+
public function test_routes_emit_failures_to_error_handler() : void
60+
{
61+
$throwingClock = new class implements ClockInterface {
62+
public function now() : \DateTimeImmutable
63+
{
64+
throw new \RuntimeException('clock blew up');
65+
}
66+
};
67+
68+
$loggerProvider = new LoggerProvider(
69+
new MemoryLogProcessor(new VoidExporter()),
70+
$throwingClock,
71+
new MemoryContextStorage(),
72+
);
73+
74+
$logger = $loggerProvider->logger(Resource::empty(), 'test-scope');
75+
$spy = new ErrorHandlerSpy();
76+
$psr3 = new TelemetryLogger($logger, errorHandler: $spy);
77+
78+
$psr3->info('hello');
79+
80+
self::assertSame(1, $spy->count());
81+
self::assertSame('clock blew up', $spy->last()?->getMessage());
82+
}
83+
84+
public function test_still_throws_on_invalid_level_per_psr3() : void
85+
{
86+
$loggerProvider = new LoggerProvider(
87+
new MemoryLogProcessor(new VoidExporter()),
88+
new SystemClock(),
89+
new MemoryContextStorage(),
90+
);
91+
92+
$logger = $loggerProvider->logger(Resource::empty(), 'test-scope');
93+
$spy = new ErrorHandlerSpy();
94+
$psr3 = new TelemetryLogger($logger, errorHandler: $spy);
95+
96+
$this->expectException(InvalidArgumentException::class);
97+
98+
$psr3->log(new \stdClass(), 'message');
99+
}
100+
}

src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/DSL/functions.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Flow\Bridge\Telemetry\OTLP\Transport\{CurlTransport, CurlTransportOptions, GrpcTransport, StreamTransport};
1010
use Flow\ETL\Attribute\{DocumentationDSL, Module, Type as DSLType};
1111
use Flow\Telemetry\Context\{ContextStorage, MemoryContextStorage};
12+
use Flow\Telemetry\ErrorHandler\{ErrorHandler, ErrorLogHandler};
1213
use Flow\Telemetry\Logger\{LogProcessor, LoggerProvider};
1314
use Flow\Telemetry\Meter\{AggregationTemporality, MetricProcessor};
1415
use Flow\Telemetry\Meter\MeterProvider;
@@ -144,11 +145,14 @@ function otlp_stream_transport(
144145
* ```
145146
*
146147
* @param Transport $transport The transport for sending telemetry data
148+
* @param ErrorHandler $errorHandler Handler for Throwables raised by the transport
147149
*/
148150
#[DocumentationDSL(module: Module::TELEMETRY_OTLP, type: DSLType::HELPER)]
149-
function otlp_exporter(Transport $transport) : OTLPExporter
150-
{
151-
return new OTLPExporter($transport);
151+
function otlp_exporter(
152+
Transport $transport,
153+
ErrorHandler $errorHandler = new ErrorLogHandler(),
154+
) : OTLPExporter {
155+
return new OTLPExporter($transport, $errorHandler);
152156
}
153157

154158
/**

src/bridge/telemetry/otlp/src/Flow/Bridge/Telemetry/OTLP/Exporter/OTLPExporter.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Flow\Bridge\Telemetry\OTLP\Exporter;
66

7+
use Flow\Telemetry\ErrorHandler\{ErrorHandler, ErrorLogHandler};
78
use Flow\Telemetry\Exporter\Exporter;
89
use Flow\Telemetry\Signal\Signals;
910
use Flow\Telemetry\Transport\Transport;
@@ -15,6 +16,7 @@
1516
{
1617
public function __construct(
1718
private Transport $transport,
19+
private ErrorHandler $errorHandler = new ErrorLogHandler(),
1820
) {
1921
}
2022

@@ -28,7 +30,9 @@ public function export(Signals $signal) : bool
2830
$this->transport->send($signal);
2931

3032
return true;
31-
} catch (\Throwable) {
33+
} catch (\Throwable $e) {
34+
$this->errorHandler->handle($e);
35+
3236
return false;
3337
}
3438
}

src/bridge/telemetry/otlp/tests/Flow/Bridge/Telemetry/OTLP/Tests/Unit/Exporter/OTLPExporterTest.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,26 @@
66

77
use Flow\Bridge\Telemetry\OTLP\Exporter\OTLPExporter;
88
use Flow\Telemetry\Attributes;
9+
use Flow\Telemetry\ErrorHandler\NullErrorHandler;
910
use Flow\Telemetry\Logger\{LogEntry, LogRecord, Severity};
1011
use Flow\Telemetry\Meter\{Metric, MetricType};
1112
use Flow\Telemetry\Signal\Signals;
12-
use Flow\Telemetry\Tests\Mother\{InstrumentationScopeMother, ResourceMother, SpanMother};
13+
use Flow\Telemetry\Tests\Mother\{ErrorHandlerSpy, InstrumentationScopeMother, ResourceMother, SpanMother};
1314
use Flow\Telemetry\Transport\{Transport, TransportException};
1415
use PHPUnit\Framework\TestCase;
1516

1617
final class OTLPExporterTest extends TestCase
1718
{
19+
public function test_export_does_not_call_error_handler_on_empty_signal() : void
20+
{
21+
$transport = new RecordingTransport();
22+
$spy = new ErrorHandlerSpy();
23+
$exporter = new OTLPExporter($transport, $spy);
24+
25+
self::assertTrue($exporter->export(Signals::logs([])));
26+
self::assertSame(0, $spy->count());
27+
}
28+
1829
public function test_export_empty_logs_returns_true_without_calling_transport() : void
1930
{
2031
$transport = new RecordingTransport();
@@ -78,9 +89,22 @@ public function test_export_metrics_calls_transport() : void
7889
public function test_export_returns_false_when_transport_throws() : void
7990
{
8091
$transport = new ThrowingTransport();
81-
$exporter = new OTLPExporter($transport);
92+
$exporter = new OTLPExporter($transport, new NullErrorHandler());
93+
94+
self::assertFalse($exporter->export(Signals::traces([SpanMother::withName('span')])));
95+
}
96+
97+
public function test_export_routes_transport_throwable_to_error_handler() : void
98+
{
99+
$transport = new ThrowingTransport();
100+
$spy = new ErrorHandlerSpy();
101+
$exporter = new OTLPExporter($transport, $spy);
82102

83103
self::assertFalse($exporter->export(Signals::traces([SpanMother::withName('span')])));
104+
self::assertSame(1, $spy->count());
105+
$last = $spy->last();
106+
self::assertInstanceOf(TransportException::class, $last);
107+
self::assertSame('boom', $last->getMessage());
84108
}
85109

86110
public function test_export_traces_calls_transport() : void

0 commit comments

Comments
 (0)