Skip to content

Commit 5c1eec6

Browse files
committed
Added error handling and logging
1 parent 0b7d64b commit 5c1eec6

File tree

5 files changed

+247
-14
lines changed

5 files changed

+247
-14
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
],
1010
"require": {
1111
"php": "^7.0",
12-
"api-clients/middleware": "^1.0",
12+
"api-clients/middleware": "^2.0",
1313
"psr/log": "^1.0"
1414
},
1515
"require-dev": {

composer.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/LoggerMiddleware.php

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
use Psr\Http\Message\ResponseInterface;
99
use Psr\Log\LoggerInterface;
1010
use React\Promise\CancellablePromiseInterface;
11+
use Throwable;
12+
use function React\Promise\reject;
1113
use function React\Promise\resolve;
1214

1315
class LoggerMiddleware implements MiddlewareInterface
1416
{
15-
const REQUEST = 'request';
17+
const REQUEST = 'request';
1618
const RESPONSE = 'response';
19+
const ERROR = 'error';
1720

1821
/**
1922
* @var LoggerInterface
@@ -95,17 +98,44 @@ public function post(ResponseInterface $response, array $options = []): Cancella
9598

9699
$message = 'Request ' . $this->requestId . ' completed.';
97100

98-
$this->context[self::RESPONSE]['status_code'] = $response->getStatusCode();
99-
$this->context[self::RESPONSE]['status_reason'] = $response->getReasonPhrase();
100-
$this->context[self::RESPONSE]['protocol_version'] = $response->getProtocolVersion();
101-
$ignoreHeaders = $options[self::class][Options::IGNORE_HEADERS] ?? [];
102-
$this->iterateHeaders(self::RESPONSE, $response->getHeaders(), $ignoreHeaders);
101+
$this->addResponseToContext($response, $options);
103102

104103
$this->logger->log($options[self::class][Options::LEVEL], $message, $this->context);
105104

106105
return resolve($response);
107106
}
108107

108+
/**
109+
* @param Throwable $throwable
110+
* @param array $options
111+
* @return CancellablePromiseInterface
112+
*/
113+
public function error(Throwable $throwable, array $options = []): CancellablePromiseInterface
114+
{
115+
if (!isset($options[self::class][Options::ERROR_LEVEL])) {
116+
return reject($throwable);
117+
}
118+
119+
$message = $throwable->getMessage();
120+
121+
$response = null;
122+
if (method_exists($throwable, 'getResponse')) {
123+
$response = $throwable->getResponse();
124+
}
125+
if ($response instanceof ResponseInterface) {
126+
$this->addResponseToContext($response, $options);
127+
}
128+
129+
$this->context[self::ERROR]['code'] = $throwable->getCode();
130+
$this->context[self::ERROR]['file'] = $throwable->getFile();
131+
$this->context[self::ERROR]['line'] = $throwable->getLine();
132+
$this->context[self::ERROR]['trace'] = $throwable->getTraceAsString();
133+
134+
$this->logger->log($options[self::class][Options::ERROR_LEVEL], $message, $this->context);
135+
136+
return reject($throwable);
137+
}
138+
109139
/**
110140
* @param string $prefix
111141
* @param array $headers
@@ -121,4 +151,13 @@ protected function iterateHeaders(string $prefix, array $headers, array $ignoreH
121151
$this->context[$prefix]['headers'][$header] = $value;
122152
}
123153
}
154+
155+
private function addResponseToContext(ResponseInterface $response, array $options)
156+
{
157+
$this->context[self::RESPONSE]['status_code'] = $response->getStatusCode();
158+
$this->context[self::RESPONSE]['status_reason'] = $response->getReasonPhrase();
159+
$this->context[self::RESPONSE]['protocol_version'] = $response->getProtocolVersion();
160+
$ignoreHeaders = $options[self::class][Options::IGNORE_HEADERS] ?? [];
161+
$this->iterateHeaders(self::RESPONSE, $response->getHeaders(), $ignoreHeaders);
162+
}
124163
}

src/Options.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ final class Options
1111
{
1212
const IGNORE_HEADERS = 'ignore_headers';
1313
const LEVEL = 'level';
14+
const ERROR_LEVEL = 'error_level';
1415
}

tests/LoggerMiddlewareTest.php

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use ApiClients\Middleware\Log\LoggerMiddleware;
77
use ApiClients\Middleware\Log\Options;
88
use ApiClients\Tools\TestUtilities\TestCase;
9+
use Exception;
910
use GuzzleHttp\Psr7\Request;
1011
use GuzzleHttp\Psr7\Response;
1112
use Prophecy\Argument;
@@ -23,11 +24,43 @@ public function testPriority()
2324
$this->assertSame(Priority::LAST, $middleware->priority());
2425
}
2526

27+
public function testNoConfig()
28+
{
29+
$options = [];
30+
$request = new Request(
31+
'GET',
32+
'https://example.com/',
33+
[
34+
'X-Foo' => 'bar',
35+
'X-Ignore-Request' => 'nope',
36+
]
37+
);
38+
$response = new Response(
39+
200,
40+
[
41+
'X-Bar' => 'foo',
42+
'X-Ignore-Response' => 'nope',
43+
]
44+
);
45+
$exception = new Exception(
46+
'New Exception'
47+
);
48+
49+
$logger = $this->prophesize(LoggerInterface::class);
50+
$logger->log(Argument::any(), Argument::any(), Argument::any())->shouldNotBeCalled();
51+
$middleware = new LoggerMiddleware($logger->reveal());
52+
$middleware->pre($request, $options);
53+
$middleware->post($response, $options);
54+
$middleware->error($exception, $options);
55+
}
56+
57+
2658
public function testLog()
2759
{
2860
$options = [
2961
LoggerMiddleware::class => [
3062
Options::LEVEL => LogLevel::DEBUG,
63+
Options::ERROR_LEVEL => LogLevel::ERROR,
3164
Options::IGNORE_HEADERS => [
3265
'X-Ignore-Request',
3366
'X-Ignore-Response',
@@ -49,6 +82,9 @@ public function testLog()
4982
'X-Ignore-Response' => 'nope',
5083
]
5184
);
85+
$exception = new Exception(
86+
'New Exception'
87+
);
5288

5389
$logger = $this->prophesize(LoggerInterface::class);
5490
$logger->log(
@@ -74,9 +110,166 @@ public function testLog()
74110
],
75111
]
76112
)->shouldBeCalled();
113+
$logger->log(
114+
LogLevel::ERROR,
115+
$exception->getMessage(),
116+
[
117+
'request' => [
118+
'method' => 'GET',
119+
'uri' => 'https://example.com/',
120+
'protocol_version' => '1.1',
121+
'headers' => [
122+
'Host' => ['example.com'],
123+
'X-Foo' => ['bar'],
124+
],
125+
],
126+
'response' => [
127+
'status_code' => 200,
128+
'status_reason' => 'OK',
129+
'protocol_version' => '1.1',
130+
'headers' => [
131+
'X-Bar' => ['foo'],
132+
],
133+
],
134+
'error' => [
135+
'code' => $exception->getCode(),
136+
'file' => $exception->getFile(),
137+
'line' => $exception->getLine(),
138+
'trace' => $exception->getTraceAsString(),
139+
],
140+
]
141+
)->shouldBeCalled();
77142

78143
$middleware = new LoggerMiddleware($logger->reveal());
79144
$middleware->pre($request, $options);
80145
$middleware->post($response, $options);
146+
$middleware->error($exception, $options);
147+
}
148+
149+
public function testLogError()
150+
{
151+
$options = [
152+
LoggerMiddleware::class => [
153+
Options::LEVEL => LogLevel::DEBUG,
154+
Options::ERROR_LEVEL => LogLevel::ERROR,
155+
Options::IGNORE_HEADERS => [
156+
'X-Ignore-Request',
157+
'X-Ignore-Response',
158+
],
159+
],
160+
];
161+
$request = new Request(
162+
'GET',
163+
'https://example.com/',
164+
[
165+
'X-Foo' => 'bar',
166+
'X-Ignore-Request' => 'nope',
167+
]
168+
);
169+
$exception = new class(
170+
'New Exception'
171+
) extends Exception {
172+
public function getResponse()
173+
{
174+
return new Response(
175+
200,
176+
[
177+
'X-Bar' => 'foo',
178+
'X-Ignore-Response' => 'nope',
179+
]
180+
);
181+
}
182+
};
183+
184+
$logger = $this->prophesize(LoggerInterface::class);
185+
$logger->log(
186+
LogLevel::ERROR,
187+
$exception->getMessage(),
188+
[
189+
'request' => [
190+
'method' => 'GET',
191+
'uri' => 'https://example.com/',
192+
'protocol_version' => '1.1',
193+
'headers' => [
194+
'Host' => ['example.com'],
195+
'X-Foo' => ['bar'],
196+
],
197+
],
198+
'response' => [
199+
'status_code' => 200,
200+
'status_reason' => 'OK',
201+
'protocol_version' => '1.1',
202+
'headers' => [
203+
'X-Bar' => ['foo'],
204+
],
205+
],
206+
'error' => [
207+
'code' => $exception->getCode(),
208+
'file' => $exception->getFile(),
209+
'line' => $exception->getLine(),
210+
'trace' => $exception->getTraceAsString(),
211+
],
212+
]
213+
)->shouldBeCalled();
214+
215+
$middleware = new LoggerMiddleware($logger->reveal());
216+
$middleware->pre($request, $options);
217+
$middleware->error($exception, $options);
218+
}
219+
220+
public function testLogErrorNoResponse()
221+
{
222+
$options = [
223+
LoggerMiddleware::class => [
224+
Options::LEVEL => LogLevel::DEBUG,
225+
Options::ERROR_LEVEL => LogLevel::ERROR,
226+
Options::IGNORE_HEADERS => [
227+
'X-Ignore-Request',
228+
'X-Ignore-Response',
229+
],
230+
],
231+
];
232+
$request = new Request(
233+
'GET',
234+
'https://example.com/',
235+
[
236+
'X-Foo' => 'bar',
237+
'X-Ignore-Request' => 'nope',
238+
]
239+
);
240+
$exception = new Exception('New Exception');
241+
242+
$logger = $this->prophesize(LoggerInterface::class);
243+
$logger->log(
244+
LogLevel::ERROR,
245+
$exception->getMessage(),
246+
[
247+
'request' => [
248+
'method' => 'GET',
249+
'uri' => 'https://example.com/',
250+
'protocol_version' => '1.1',
251+
'headers' => [
252+
'Host' => ['example.com'],
253+
'X-Foo' => ['bar'],
254+
],
255+
],
256+
'response' => [
257+
'status_code' => null,
258+
'status_reason' => null,
259+
'protocol_version' => null,
260+
'headers' => [],
261+
],
262+
'error' => [
263+
'code' => $exception->getCode(),
264+
'file' => $exception->getFile(),
265+
'line' => $exception->getLine(),
266+
'trace' => $exception->getTraceAsString(),
267+
],
268+
]
269+
)->shouldBeCalled();
270+
271+
$middleware = new LoggerMiddleware($logger->reveal());
272+
$middleware->pre($request, $options);
273+
$middleware->error($exception, $options);
81274
}
82275
}

0 commit comments

Comments
 (0)