CubeHandler.php 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog\Handler;
  11. use Monolog\Level;
  12. use Monolog\Utils;
  13. use Monolog\LogRecord;
  14. /**
  15. * Logs to Cube.
  16. *
  17. * @link http://square.github.com/cube/
  18. * @author Wan Chen <kami@kamisama.me>
  19. */
  20. class CubeHandler extends AbstractProcessingHandler
  21. {
  22. private ?\Socket $udpConnection = null;
  23. private ?\CurlHandle $httpConnection = null;
  24. private string $scheme;
  25. private string $host;
  26. private int $port;
  27. /** @var string[] */
  28. private array $acceptedSchemes = ['http', 'udp'];
  29. /**
  30. * Create a Cube handler
  31. *
  32. * @throws \UnexpectedValueException when given url is not a valid url.
  33. * A valid url must consist of three parts : protocol://host:port
  34. * Only valid protocols used by Cube are http and udp
  35. */
  36. public function __construct(string $url, int|string|Level $level = Level::Debug, bool $bubble = true)
  37. {
  38. $urlInfo = parse_url($url);
  39. if ($urlInfo === false || !isset($urlInfo['scheme'], $urlInfo['host'], $urlInfo['port'])) {
  40. throw new \UnexpectedValueException('URL "'.$url.'" is not valid');
  41. }
  42. if (!in_array($urlInfo['scheme'], $this->acceptedSchemes, true)) {
  43. throw new \UnexpectedValueException(
  44. 'Invalid protocol (' . $urlInfo['scheme'] . ').'
  45. . ' Valid options are ' . implode(', ', $this->acceptedSchemes)
  46. );
  47. }
  48. $this->scheme = $urlInfo['scheme'];
  49. $this->host = $urlInfo['host'];
  50. $this->port = $urlInfo['port'];
  51. parent::__construct($level, $bubble);
  52. }
  53. /**
  54. * Establish a connection to an UDP socket
  55. *
  56. * @throws \LogicException when unable to connect to the socket
  57. * @throws MissingExtensionException when there is no socket extension
  58. */
  59. protected function connectUdp(): void
  60. {
  61. if (!extension_loaded('sockets')) {
  62. throw new MissingExtensionException('The sockets extension is required to use udp URLs with the CubeHandler');
  63. }
  64. $udpConnection = socket_create(AF_INET, SOCK_DGRAM, 0);
  65. if (false === $udpConnection) {
  66. throw new \LogicException('Unable to create a socket');
  67. }
  68. $this->udpConnection = $udpConnection;
  69. if (!socket_connect($this->udpConnection, $this->host, $this->port)) {
  70. throw new \LogicException('Unable to connect to the socket at ' . $this->host . ':' . $this->port);
  71. }
  72. }
  73. /**
  74. * Establish a connection to an http server
  75. *
  76. * @throws \LogicException when unable to connect to the socket
  77. * @throws MissingExtensionException when no curl extension
  78. */
  79. protected function connectHttp(): void
  80. {
  81. if (!extension_loaded('curl')) {
  82. throw new MissingExtensionException('The curl extension is required to use http URLs with the CubeHandler');
  83. }
  84. $httpConnection = curl_init('http://'.$this->host.':'.$this->port.'/1.0/event/put');
  85. if (false === $httpConnection) {
  86. throw new \LogicException('Unable to connect to ' . $this->host . ':' . $this->port);
  87. }
  88. $this->httpConnection = $httpConnection;
  89. curl_setopt($this->httpConnection, CURLOPT_CUSTOMREQUEST, "POST");
  90. curl_setopt($this->httpConnection, CURLOPT_RETURNTRANSFER, true);
  91. }
  92. /**
  93. * @inheritDoc
  94. */
  95. protected function write(LogRecord $record): void
  96. {
  97. $date = $record->datetime;
  98. $data = ['time' => $date->format('Y-m-d\TH:i:s.uO')];
  99. $context = $record->context;
  100. if (isset($context['type'])) {
  101. $data['type'] = $context['type'];
  102. unset($context['type']);
  103. } else {
  104. $data['type'] = $record->channel;
  105. }
  106. $data['data'] = $context;
  107. $data['data']['level'] = $record->level;
  108. if ($this->scheme === 'http') {
  109. $this->writeHttp(Utils::jsonEncode($data));
  110. } else {
  111. $this->writeUdp(Utils::jsonEncode($data));
  112. }
  113. }
  114. private function writeUdp(string $data): void
  115. {
  116. if (null === $this->udpConnection) {
  117. $this->connectUdp();
  118. }
  119. if (null === $this->udpConnection) {
  120. throw new \LogicException('No UDP socket could be opened');
  121. }
  122. socket_send($this->udpConnection, $data, strlen($data), 0);
  123. }
  124. private function writeHttp(string $data): void
  125. {
  126. if (null === $this->httpConnection) {
  127. $this->connectHttp();
  128. }
  129. if (null === $this->httpConnection) {
  130. throw new \LogicException('No connection could be established');
  131. }
  132. curl_setopt($this->httpConnection, CURLOPT_POSTFIELDS, '['.$data.']');
  133. curl_setopt($this->httpConnection, CURLOPT_HTTPHEADER, [
  134. 'Content-Type: application/json',
  135. 'Content-Length: ' . strlen('['.$data.']'),
  136. ]);
  137. Curl\Util::execute($this->httpConnection, 5, false);
  138. }
  139. }