Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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