MessageFormatter.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <?php
  2. namespace GuzzleHttp;
  3. use Psr\Http\Message\MessageInterface;
  4. use Psr\Http\Message\RequestInterface;
  5. use Psr\Http\Message\ResponseInterface;
  6. /**
  7. * Formats log messages using variable substitutions for requests, responses,
  8. * and other transactional data.
  9. *
  10. * The following variable substitutions are supported:
  11. *
  12. * - {request}: Full HTTP request message
  13. * - {response}: Full HTTP response message
  14. * - {ts}: ISO 8601 date in GMT
  15. * - {date_iso_8601} ISO 8601 date in GMT
  16. * - {date_common_log} Apache common log date using the configured timezone.
  17. * - {host}: Host of the request
  18. * - {method}: Method of the request
  19. * - {uri}: URI of the request
  20. * - {version}: Protocol version
  21. * - {target}: Request target of the request (path + query + fragment)
  22. * - {hostname}: Hostname of the machine that sent the request
  23. * - {code}: Status code of the response (if available)
  24. * - {phrase}: Reason phrase of the response (if available)
  25. * - {error}: Any error messages (if available)
  26. * - {req_header_*}: Replace `*` with the lowercased name of a request header to add to the message
  27. * - {res_header_*}: Replace `*` with the lowercased name of a response header to add to the message
  28. * - {req_headers}: Request headers
  29. * - {res_headers}: Response headers
  30. * - {req_body}: Request body
  31. * - {res_body}: Response body
  32. *
  33. * @final
  34. */
  35. class MessageFormatter implements MessageFormatterInterface
  36. {
  37. /**
  38. * Apache Common Log Format.
  39. *
  40. * @see https://httpd.apache.org/docs/2.4/logs.html#common
  41. *
  42. * @var string
  43. */
  44. public const CLF = '{hostname} {req_header_User-Agent} - [{date_common_log}] "{method} {target} HTTP/{version}" {code} {res_header_Content-Length}';
  45. public const DEBUG = ">>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}";
  46. public const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}';
  47. /**
  48. * @var string Template used to format log messages
  49. */
  50. private $template;
  51. /**
  52. * @param string $template Log message template
  53. */
  54. public function __construct(?string $template = self::CLF)
  55. {
  56. $this->template = $template ?: self::CLF;
  57. }
  58. /**
  59. * Returns a formatted message string.
  60. *
  61. * @param RequestInterface $request Request that was sent
  62. * @param ResponseInterface|null $response Response that was received
  63. * @param \Throwable|null $error Exception that was received
  64. */
  65. public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null): string
  66. {
  67. $cache = [];
  68. /** @var string */
  69. return \preg_replace_callback(
  70. '/{\s*([A-Za-z_\-\.0-9]+)\s*}/',
  71. function (array $matches) use ($request, $response, $error, &$cache) {
  72. if (isset($cache[$matches[1]])) {
  73. return $cache[$matches[1]];
  74. }
  75. $result = '';
  76. switch ($matches[1]) {
  77. case 'request':
  78. $result = Psr7\Message::toString($request);
  79. break;
  80. case 'response':
  81. $result = $response ? Psr7\Message::toString($response) : '';
  82. break;
  83. case 'req_headers':
  84. $result = \trim($request->getMethod()
  85. .' '.$request->getRequestTarget())
  86. .' HTTP/'.$request->getProtocolVersion()."\r\n"
  87. .$this->headers($request);
  88. break;
  89. case 'res_headers':
  90. $result = $response ?
  91. \sprintf(
  92. 'HTTP/%s %d %s',
  93. $response->getProtocolVersion(),
  94. $response->getStatusCode(),
  95. $response->getReasonPhrase()
  96. )."\r\n".$this->headers($response)
  97. : 'NULL';
  98. break;
  99. case 'req_body':
  100. $result = $request->getBody()->__toString();
  101. break;
  102. case 'res_body':
  103. if (!$response instanceof ResponseInterface) {
  104. $result = 'NULL';
  105. break;
  106. }
  107. $body = $response->getBody();
  108. if (!$body->isSeekable()) {
  109. $result = 'RESPONSE_NOT_LOGGEABLE';
  110. break;
  111. }
  112. $result = $response->getBody()->__toString();
  113. break;
  114. case 'ts':
  115. case 'date_iso_8601':
  116. $result = \gmdate('c');
  117. break;
  118. case 'date_common_log':
  119. $result = \date('d/M/Y:H:i:s O');
  120. break;
  121. case 'method':
  122. $result = $request->getMethod();
  123. break;
  124. case 'version':
  125. $result = $request->getProtocolVersion();
  126. break;
  127. case 'uri':
  128. case 'url':
  129. $result = $request->getUri()->__toString();
  130. break;
  131. case 'target':
  132. $result = $request->getRequestTarget();
  133. break;
  134. case 'req_version':
  135. $result = $request->getProtocolVersion();
  136. break;
  137. case 'res_version':
  138. $result = $response
  139. ? $response->getProtocolVersion()
  140. : 'NULL';
  141. break;
  142. case 'host':
  143. $result = $request->getHeaderLine('Host');
  144. break;
  145. case 'hostname':
  146. $result = \gethostname();
  147. break;
  148. case 'code':
  149. $result = $response ? $response->getStatusCode() : 'NULL';
  150. break;
  151. case 'phrase':
  152. $result = $response ? $response->getReasonPhrase() : 'NULL';
  153. break;
  154. case 'error':
  155. $result = $error ? $error->getMessage() : 'NULL';
  156. break;
  157. default:
  158. // handle prefixed dynamic headers
  159. if (\strpos($matches[1], 'req_header_') === 0) {
  160. $result = $request->getHeaderLine(\substr($matches[1], 11));
  161. } elseif (\strpos($matches[1], 'res_header_') === 0) {
  162. $result = $response
  163. ? $response->getHeaderLine(\substr($matches[1], 11))
  164. : 'NULL';
  165. }
  166. }
  167. $cache[$matches[1]] = $result;
  168. return $result;
  169. },
  170. $this->template
  171. );
  172. }
  173. /**
  174. * Get headers from message as string
  175. */
  176. private function headers(MessageInterface $message): string
  177. {
  178. $result = '';
  179. foreach ($message->getHeaders() as $name => $values) {
  180. $result .= $name.': '.\implode(', ', $values)."\r\n";
  181. }
  182. return \trim($result);
  183. }
  184. }