Passed
Push — main ( 2a1e53...db5ef1 )
by Brian
02:40
created

GuzzleHttpLogMiddleware::useLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Bmatovu\AirtelMoney\Support;
6
7
use Illuminate\Container\Container;
8
use Illuminate\Contracts\Config\Repository;
9
use Illuminate\Support\Str;
10
use Psr\Http\Message\RequestInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\UriInterface;
13
use Psr\Log\LoggerInterface;
14
use Symfony\Component\HttpFoundation\Response;
15
16
final class GuzzleHttpLogMiddleware
17
{
18
    private LoggerInterface $logger;
19
20
    private array $mask = ['Authorization', 'Cookie', 'Set-Cookie', 'X-Xsrf-Token'];
21
22
    private array $only = ['Authorization', 'Accept', 'X-Request-Id', 'Content-Type', 'Content-Length', 'Connection'];
23
24
    private array $hide = ['User-Agent', 'Host', 'Date', 'Postman-Token', 'Php-Auth-Pw', 'Php-Auth-User'];
25
26
    private array $skip = ['/token'];
27
28
    private int $size = 0;
29
30
    private string|float $start = 0;
31
32 29
    public function __construct()
33
    {
34 29
        $this->start = microtime(true);
35
36 29
        $app = Container::getInstance();
37
38 29
        $this->logger = $app->make(LoggerInterface::class);
39
40 29
        $config = $app->make(Repository::class);
41
42 29
        $this->mask = array_map('strtolower', $config->get('logging.http.mask', $this->mask));
43 29
        $this->only = array_map('strtolower', $config->get('logging.http.only', $this->only));
44 29
        $this->hide = array_map('strtolower', $config->get('logging.http.hide', $this->hide));
45 29
        $this->skip = $config->get('logging.http.skip', $this->skip);
46 29
        $this->size = $config->get('logging.http.size', $this->size);
47
    }
48
49
    // --- Fluent Configuration API ---
50
51
    public function useLogger(LoggerInterface $logger): self
52
    {
53
        $this->logger = $logger;
54
55
        return $this;
56
    }
57
58
    public function useMask(array $mask): self
59
    {
60
        $this->mask = array_map('strtolower', $mask);
61
62
        return $this;
63
    }
64
65
    public function useOnly(array $only): self
66
    {
67
        $this->only = $only ? array_map('strtolower', $only) : null;
68
69
        return $this;
70
    }
71
72
    public function useHide(array $hide): self
73
    {
74
        $this->hide = $hide ? array_map('strtolower', $hide) : null;
75
76
        return $this;
77
    }
78
79
    public function useSkip(array $skip): self
80
    {
81
        $this->skip = $skip;
82
83
        return $this;
84
    }
85
86
    public function useSize(int $length): self
87
    {
88
        $this->size = $length;
89
90
        return $this;
91
    }
92
93
    // --- Middleware Entry Point ---
94
95
    public function __invoke(callable $handler): callable
96
    {
97
        return function (RequestInterface $request, array $options) use ($handler) {
98
            // $start = microtime(true);
99
            $requestId = $_SERVER['REQUEST_ID'] ?? Str::random(8);
100
            $request = $request->withHeader('X-Request-Id', $requestId);
101
102
            $maskRequest = $this->filterAndMaskRequest($request);
103
104
            $this->logger->info(sprintf(
105
                'HTTP_OUT [Request] %s %s HTTP/%s',
106
                $maskRequest->getMethod(),
107
                $maskRequest->getUri(),
108
                $maskRequest->getProtocolVersion()
109
            ));
110
111
            if (! $this->shouldSkip($request->getUri())) {
0 ignored issues
show
Bug introduced by
The method getUri() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Psr\Http\Message\RequestInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

111
            if (! $this->shouldSkip($request->/** @scrutinizer ignore-call */ getUri())) {
Loading history...
112
                $this->logger->debug('HTTP_OUT [Request] Headers', $this->formatHeaders($maskRequest));
113
                $this->logger->debug('HTTP_OUT [Request] Body '.$this->trimBody((string) $maskRequest->getBody()));
114
            }
115
116
            return $handler($request, $options)->then(
117
                function (ResponseInterface $response) use ($request): ResponseInterface {
118
                    $duration = (int) (microtime(true) - $this->start) * 1000;
119
                    // $duration = (int) (microtime(true) - LARAVEL_START) * 1000;
120
121
                    $maskResponse = $this->filterAndMaskResponse($response);
122
123
                    $this->logger->info(sprintf(
124
                        'HTTP_OUT [Response] HTTP/%s %s %s %dms',
125
                        $maskResponse->getProtocolVersion(),
126
                        $maskResponse->getStatusCode(),
127
                        Response::$statusTexts[$maskResponse->getStatusCode()] ?? '',
128
                        $duration
129
                    ));
130
131
                    if (! $this->shouldSkip($request->getUri())) {
132
                        $this->logger->debug('HTTP_OUT [Response] Headers', $this->formatHeaders($maskResponse));
133
                        $this->logger->debug('HTTP_OUT [Response] Body '.$this->trimBody((string) $maskResponse->getBody()));
134
                    }
135
136
                    return $response;
137
                }
138
            );
139
        };
140
    }
141
142
    // --- Internals ---
143
144
    private function filterAndMaskRequest(RequestInterface $request): RequestInterface
145
    {
146
        foreach ($request->getHeaders() as $name => $values) {
147
            $name = strtolower((string) $name);
148
149
            if ($this->shouldRemoveHeader($name)) {
150
                $request = $request->withoutHeader($name);
151
152
                continue;
153
            }
154
155
            if (in_array($name, $this->mask, true)) {
156
                $request = $request->withHeader($name, ['**********']);
157
            }
158
        }
159
160
        return $request;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $request could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\RequestInterface. Consider adding an additional type-check to rule them out.
Loading history...
161
    }
162
163
    private function filterAndMaskResponse(ResponseInterface $response): ResponseInterface
164
    {
165
        foreach ($response->getHeaders() as $name => $values) {
166
            $name = strtolower((string) $name);
167
168
            if ($this->shouldRemoveHeader($name)) {
169
                $response = $response->withoutHeader($name);
170
171
                continue;
172
            }
173
174
            if (in_array($name, $this->mask, true)) {
175
                $response = $response->withHeader($name, ['**********']);
176
            }
177
        }
178
179
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response could return the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
180
    }
181
182
    private function shouldRemoveHeader(string $name): bool
183
    {
184
        if ($this->hide && in_array($name, $this->hide)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hide of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
185
            return true;
186
        }
187
188
        if ($this->only && ! in_array($name, $this->only)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->only of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
189
            return true;
190
        }
191
192
        return false;
193
    }
194
195
    private function formatHeaders($message): array
196
    {
197
        $headers = [];
198
199
        foreach ($message->getHeaders() as $name => $values) {
200
            $headers[$name] = count($values) > 1 ? $values : $values[0];
201
        }
202
203
        return $headers;
204
    }
205
206
    private function trimBody(string $body): string
207
    {
208
        // $length = strlen($body);
209
210
        // return $length > $this->size
211
        //     ? substr($body, 0, $this->size).sprintf('... [truncated, %d bytes total]', $length)
212
        //     : $body;
213
214
        if (! $this->size) {
215
            return $body;
216
        }
217
218
        return Str::limit($body, $this->size, end: ' ... [truncated]', preserveWords: true);
219
    }
220
221
    private function shouldSkip(UriInterface $uri): bool
222
    {
223
        if (empty($this->skip)) {
224
            return false;
225
        }
226
227
        $path = $uri->getPath();
228
        foreach ($this->skip as $skipPath) {
229
            if (stripos($path, $skipPath) !== false) {
230
                return true;
231
            }
232
        }
233
234
        return false;
235
    }
236
}
237