SlackRecord.php 9.6 KB

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