PageRenderTime 47ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/monolog/monolog/src/Monolog/Handler/Slack/SlackRecord.php

https://gitlab.com/madwanz64/laravel
PHP | 387 lines | 215 code | 58 blank | 114 comment | 23 complexity | 476af486dfbb66207844f18feaaff72c MD5 | raw file
  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\Slack;
  11. use Monolog\Logger;
  12. use Monolog\Utils;
  13. use Monolog\Formatter\NormalizerFormatter;
  14. use Monolog\Formatter\FormatterInterface;
  15. /**
  16. * Slack record utility helping to log to Slack webhooks or API.
  17. *
  18. * @author Greg Kedzierski <greg@gregkedzierski.com>
  19. * @author Haralan Dobrev <hkdobrev@gmail.com>
  20. * @see https://api.slack.com/incoming-webhooks
  21. * @see https://api.slack.com/docs/message-attachments
  22. *
  23. * @phpstan-import-type FormattedRecord from \Monolog\Handler\AbstractProcessingHandler
  24. * @phpstan-import-type Record from \Monolog\Logger
  25. */
  26. class SlackRecord
  27. {
  28. public const COLOR_DANGER = 'danger';
  29. public const COLOR_WARNING = 'warning';
  30. public const COLOR_GOOD = 'good';
  31. public const COLOR_DEFAULT = '#e3e4e6';
  32. /**
  33. * Slack channel (encoded ID or name)
  34. * @var string|null
  35. */
  36. private $channel;
  37. /**
  38. * Name of a bot
  39. * @var string|null
  40. */
  41. private $username;
  42. /**
  43. * User icon e.g. 'ghost', 'http://example.com/user.png'
  44. * @var string|null
  45. */
  46. private $userIcon;
  47. /**
  48. * Whether the message should be added to Slack as attachment (plain text otherwise)
  49. * @var bool
  50. */
  51. private $useAttachment;
  52. /**
  53. * Whether the the context/extra messages added to Slack as attachments are in a short style
  54. * @var bool
  55. */
  56. private $useShortAttachment;
  57. /**
  58. * Whether the attachment should include context and extra data
  59. * @var bool
  60. */
  61. private $includeContextAndExtra;
  62. /**
  63. * Dot separated list of fields to exclude from slack message. E.g. ['context.field1', 'extra.field2']
  64. * @var string[]
  65. */
  66. private $excludeFields;
  67. /**
  68. * @var ?FormatterInterface
  69. */
  70. private $formatter;
  71. /**
  72. * @var NormalizerFormatter
  73. */
  74. private $normalizerFormatter;
  75. /**
  76. * @param string[] $excludeFields
  77. */
  78. public function __construct(
  79. ?string $channel = null,
  80. ?string $username = null,
  81. bool $useAttachment = true,
  82. ?string $userIcon = null,
  83. bool $useShortAttachment = false,
  84. bool $includeContextAndExtra = false,
  85. array $excludeFields = array(),
  86. FormatterInterface $formatter = null
  87. ) {
  88. $this
  89. ->setChannel($channel)
  90. ->setUsername($username)
  91. ->useAttachment($useAttachment)
  92. ->setUserIcon($userIcon)
  93. ->useShortAttachment($useShortAttachment)
  94. ->includeContextAndExtra($includeContextAndExtra)
  95. ->excludeFields($excludeFields)
  96. ->setFormatter($formatter);
  97. if ($this->includeContextAndExtra) {
  98. $this->normalizerFormatter = new NormalizerFormatter();
  99. }
  100. }
  101. /**
  102. * Returns required data in format that Slack
  103. * is expecting.
  104. *
  105. * @phpstan-param FormattedRecord $record
  106. * @phpstan-return mixed[]
  107. */
  108. public function getSlackData(array $record): array
  109. {
  110. $dataArray = array();
  111. $record = $this->removeExcludedFields($record);
  112. if ($this->username) {
  113. $dataArray['username'] = $this->username;
  114. }
  115. if ($this->channel) {
  116. $dataArray['channel'] = $this->channel;
  117. }
  118. if ($this->formatter && !$this->useAttachment) {
  119. /** @phpstan-ignore-next-line */
  120. $message = $this->formatter->format($record);
  121. } else {
  122. $message = $record['message'];
  123. }
  124. if ($this->useAttachment) {
  125. $attachment = array(
  126. 'fallback' => $message,
  127. 'text' => $message,
  128. 'color' => $this->getAttachmentColor($record['level']),
  129. 'fields' => array(),
  130. 'mrkdwn_in' => array('fields'),
  131. 'ts' => $record['datetime']->getTimestamp(),
  132. 'footer' => $this->username,
  133. 'footer_icon' => $this->userIcon,
  134. );
  135. if ($this->useShortAttachment) {
  136. $attachment['title'] = $record['level_name'];
  137. } else {
  138. $attachment['title'] = 'Message';
  139. $attachment['fields'][] = $this->generateAttachmentField('Level', $record['level_name']);
  140. }
  141. if ($this->includeContextAndExtra) {
  142. foreach (array('extra', 'context') as $key) {
  143. if (empty($record[$key])) {
  144. continue;
  145. }
  146. if ($this->useShortAttachment) {
  147. $attachment['fields'][] = $this->generateAttachmentField(
  148. (string) $key,
  149. $record[$key]
  150. );
  151. } else {
  152. // Add all extra fields as individual fields in attachment
  153. $attachment['fields'] = array_merge(
  154. $attachment['fields'],
  155. $this->generateAttachmentFields($record[$key])
  156. );
  157. }
  158. }
  159. }
  160. $dataArray['attachments'] = array($attachment);
  161. } else {
  162. $dataArray['text'] = $message;
  163. }
  164. if ($this->userIcon) {
  165. if (filter_var($this->userIcon, FILTER_VALIDATE_URL)) {
  166. $dataArray['icon_url'] = $this->userIcon;
  167. } else {
  168. $dataArray['icon_emoji'] = ":{$this->userIcon}:";
  169. }
  170. }
  171. return $dataArray;
  172. }
  173. /**
  174. * Returns a Slack message attachment color associated with
  175. * provided level.
  176. */
  177. public function getAttachmentColor(int $level): string
  178. {
  179. switch (true) {
  180. case $level >= Logger::ERROR:
  181. return static::COLOR_DANGER;
  182. case $level >= Logger::WARNING:
  183. return static::COLOR_WARNING;
  184. case $level >= Logger::INFO:
  185. return static::COLOR_GOOD;
  186. default:
  187. return static::COLOR_DEFAULT;
  188. }
  189. }
  190. /**
  191. * Stringifies an array of key/value pairs to be used in attachment fields
  192. *
  193. * @param mixed[] $fields
  194. */
  195. public function stringify(array $fields): string
  196. {
  197. /** @var Record $fields */
  198. $normalized = $this->normalizerFormatter->format($fields);
  199. $hasSecondDimension = count(array_filter($normalized, 'is_array'));
  200. $hasNonNumericKeys = !count(array_filter(array_keys($normalized), 'is_numeric'));
  201. return $hasSecondDimension || $hasNonNumericKeys
  202. ? Utils::jsonEncode($normalized, JSON_PRETTY_PRINT|Utils::DEFAULT_JSON_FLAGS)
  203. : Utils::jsonEncode($normalized, Utils::DEFAULT_JSON_FLAGS);
  204. }
  205. /**
  206. * Channel used by the bot when posting
  207. *
  208. * @param ?string $channel
  209. *
  210. * @return static
  211. */
  212. public function setChannel(?string $channel = null): self
  213. {
  214. $this->channel = $channel;
  215. return $this;
  216. }
  217. /**
  218. * Username used by the bot when posting
  219. *
  220. * @param ?string $username
  221. *
  222. * @return static
  223. */
  224. public function setUsername(?string $username = null): self
  225. {
  226. $this->username = $username;
  227. return $this;
  228. }
  229. public function useAttachment(bool $useAttachment = true): self
  230. {
  231. $this->useAttachment = $useAttachment;
  232. return $this;
  233. }
  234. public function setUserIcon(?string $userIcon = null): self
  235. {
  236. $this->userIcon = $userIcon;
  237. if (\is_string($userIcon)) {
  238. $this->userIcon = trim($userIcon, ':');
  239. }
  240. return $this;
  241. }
  242. public function useShortAttachment(bool $useShortAttachment = false): self
  243. {
  244. $this->useShortAttachment = $useShortAttachment;
  245. return $this;
  246. }
  247. public function includeContextAndExtra(bool $includeContextAndExtra = false): self
  248. {
  249. $this->includeContextAndExtra = $includeContextAndExtra;
  250. if ($this->includeContextAndExtra) {
  251. $this->normalizerFormatter = new NormalizerFormatter();
  252. }
  253. return $this;
  254. }
  255. /**
  256. * @param string[] $excludeFields
  257. */
  258. public function excludeFields(array $excludeFields = []): self
  259. {
  260. $this->excludeFields = $excludeFields;
  261. return $this;
  262. }
  263. public function setFormatter(?FormatterInterface $formatter = null): self
  264. {
  265. $this->formatter = $formatter;
  266. return $this;
  267. }
  268. /**
  269. * Generates attachment field
  270. *
  271. * @param string|mixed[] $value
  272. *
  273. * @return array{title: string, value: string, short: false}
  274. */
  275. private function generateAttachmentField(string $title, $value): array
  276. {
  277. $value = is_array($value)
  278. ? sprintf('```%s```', substr($this->stringify($value), 0, 1990))
  279. : $value;
  280. return array(
  281. 'title' => ucfirst($title),
  282. 'value' => $value,
  283. 'short' => false,
  284. );
  285. }
  286. /**
  287. * Generates a collection of attachment fields from array
  288. *
  289. * @param mixed[] $data
  290. *
  291. * @return array<array{title: string, value: string, short: false}>
  292. */
  293. private function generateAttachmentFields(array $data): array
  294. {
  295. /** @var Record $data */
  296. $normalized = $this->normalizerFormatter->format($data);
  297. $fields = array();
  298. foreach ($normalized as $key => $value) {
  299. $fields[] = $this->generateAttachmentField((string) $key, $value);
  300. }
  301. return $fields;
  302. }
  303. /**
  304. * Get a copy of record with fields excluded according to $this->excludeFields
  305. *
  306. * @phpstan-param FormattedRecord $record
  307. *
  308. * @return mixed[]
  309. */
  310. private function removeExcludedFields(array $record): array
  311. {
  312. foreach ($this->excludeFields as $field) {
  313. $keys = explode('.', $field);
  314. $node = &$record;
  315. $lastKey = end($keys);
  316. foreach ($keys as $key) {
  317. if (!isset($node[$key])) {
  318. break;
  319. }
  320. if ($lastKey === $key) {
  321. unset($node[$key]);
  322. break;
  323. }
  324. $node = &$node[$key];
  325. }
  326. }
  327. return $record;
  328. }
  329. }