Completed
Push — master ( 2b4fcc...300148 )
by Sergey
04:38
created

SlackTarget   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 276
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Test Coverage

Coverage 99.16%

Importance

Changes 0
Metric Value
wmc 28
lcom 2
cbo 9
dl 0
loc 276
ccs 118
cts 119
cp 0.9916
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 5 1
A export() 0 13 2
A encodeMessage() 0 4 1
A getPayload() 0 12 1
A insertSessionIntoAttachment() 0 14 4
A insertTracesIntoAttachment() 0 11 1
A insertUserIntoAttachment() 0 13 4
A insertRequestIntoAttachment() 0 18 3
A insertIntoPayload() 0 6 2
C formatMessageAttachment() 0 53 9
1
<?php
2
/**
3
 * Slack log target for Yii 2.
4
 *
5
 * @see       https://github.com/sergeymakinen/yii2-slack-log
6
 * @copyright Copyright (c) 2016 Sergey Makinen (https://makinen.ru)
7
 * @license   https://github.com/sergeymakinen/yii2-slack-log/blob/master/LICENSE The MIT License
8
 */
9
10
namespace sergeymakinen\log;
11
12
use yii\base\InvalidValueException;
13
use yii\di\Instance;
14
use yii\helpers\Json;
15
use yii\helpers\Url;
16
use yii\helpers\VarDumper;
17
use yii\httpclient\Client;
18
use yii\log\Logger;
19
use yii\log\Target;
20
use yii\web\Request;
21
22
class SlackTarget extends Target
23
{
24
    /**
25
     * Yii HTTP client configuration.
26
     * This can be a component ID, a configuration array or a Client instance.
27
     *
28
     * @var Client|array|string
29
     * @since 1.2
30
     */
31
    public $httpClient = [
32
        'class' => 'yii\httpclient\Client',
33
    ];
34
35
    /**
36
     * Incoming Webhook URL.
37
     *
38
     * @var string
39
     */
40
    public $webhookUrl;
41
42
    /**
43
     * Displayed username.
44
     *
45
     * @var string
46
     */
47
    public $username;
48
49
    /**
50
     * Icon URL.
51
     *
52
     * @var string
53
     */
54
    public $iconUrl;
55
56
    /**
57
     * Icon Emoji.
58
     *
59
     * @var string
60
     */
61
    public $iconEmoji;
62
63
    /**
64
     * Channel or Direct Message.
65
     *
66
     * @var string
67
     */
68
    public $channel;
69
70
    /**
71
     * Colors per a Logger level.
72
     *
73
     * @var array
74
     */
75
    public $colors = [
76
        Logger::LEVEL_ERROR => 'danger',
77
        Logger::LEVEL_WARNING => 'warning',
78
    ];
79
80
    /**
81
     * @inheritDoc
82
     */
83
    public $exportInterval = 50;
84
85
    /**
86
     * @inheritDoc
87
     */
88 5
    public function init()
89
    {
90 5
        parent::init();
91 5
        $this->httpClient = Instance::ensure($this->httpClient, Client::className());
92 5
    }
93
94
    /**
95
     * @inheritDoc
96
     */
97 1
    public function export()
98
    {
99 1
        $response = $this->httpClient->post(
100 1
            $this->webhookUrl,
101 1
            Json::encode($this->getPayload()),
102 1
            ['Content-Type: application/json; charset=UTF-8']
103 1
        )->send();
104 1
        if (!$response->getIsOk()) {
105 1
            throw new InvalidValueException(
106 1
                "Unable to send logs to Slack: {$response->getContent()}", (int) $response->getStatusCode()
107 1
            );
108
        }
109
    }
110
111
    /**
112
     * Encodes special chars in a message as HTML entities.
113
     *
114
     * @param string $message
115
     *
116
     * @return string
117
     */
118 4
    protected function encodeMessage($message)
119
    {
120 4
        return htmlspecialchars($message, ENT_NOQUOTES, 'UTF-8');
121
    }
122
123
    /**
124
     * Returns a Slack API payload.
125
     *
126
     * @return array
127
     * @since 1.2
128
     */
129 2
    protected function getPayload()
130
    {
131
        $payload = [
132 2
            'parse' => 'none',
133 2
            'attachments' => array_map([$this, 'formatMessageAttachment'], $this->messages),
134 2
        ];
135 2
        $this->insertIntoPayload($payload, 'username', $this->username);
136 2
        $this->insertIntoPayload($payload, 'icon_url', $this->iconUrl);
137 2
        $this->insertIntoPayload($payload, 'icon_emoji', $this->iconEmoji);
138 2
        $this->insertIntoPayload($payload, 'channel', $this->channel);
139 2
        return $payload;
140
    }
141
142
    /**
143
     * Returns a properly formatted message attachment for Slack API.
144
     *
145
     * @param array $message
146
     *
147
     * @return array
148
     */
149 3
    protected function formatMessageAttachment($message)
150
    {
151 3
        list($text, $level, $category, $timestamp) = $message;
152
        $attachment = [
153 3
            'fallback' => $this->encodeMessage($this->formatMessage($message)),
154 3
            'title' => ucwords(Logger::getLevelName($level)),
155
            'fields' => [
156
                [
157 3
                    'title' => 'Level',
158 3
                    'value' => Logger::getLevelName($level),
159 3
                    'short' => true,
160 3
                ],
161
                [
162 3
                    'title' => 'Category',
163 3
                    'value' => '`' . $this->encodeMessage($category) . '`',
164 3
                    'short' => true,
165 3
                ],
166 3
            ],
167 3
            'footer' => static::className(),
168 3
            'ts' => (int) round($timestamp),
169
            'mrkdwn_in' => [
170 3
                'fields',
171 3
                'text',
172 3
            ],
173 3
        ];
174 3
        if (isset($this->prefix)) {
175 3
            $attachment['fields'][] = [
176 3
                'title' => 'Prefix',
177 3
                'value' => '`' . $this->encodeMessage(call_user_func($this->prefix, $message)) . '`',
178 3
                'short' => true,
179
            ];
180 3
        }
181 3
        if (isset(\Yii::$app)) {
182 3
            $this->insertRequestIntoAttachment($attachment);
183 3
            $this->insertUserIntoAttachment($attachment);
184 3
            $this->insertSessionIntoAttachment($attachment);
185 3
        }
186 3
        if (isset($this->colors[$level])) {
187 2
            $attachment['color'] = $this->colors[$level];
188 2
        }
189 3
        if (!is_string($text)) {
190 2
            if ($text instanceof \Throwable || $text instanceof \Exception) {
191 1
                $text = (string) $text;
192 1
            } else {
193 1
                $text = VarDumper::export($text);
194
            }
195 2
        }
196 3
        $attachment['text'] = "```\n" . $this->encodeMessage($text) . "\n```";
197 3
        if (isset($message[4]) && !empty($message[4])) {
198 1
            $this->insertTracesIntoAttachment($message[4], $attachment);
199 1
        }
200 3
        return $attachment;
201
    }
202
203
    /**
204
     * Inserts session data into the attachement if applicable.
205
     *
206
     * @param array $attachment
207
     */
208 3
    private function insertSessionIntoAttachment(array &$attachment)
209
    {
210
        if (
211 3
            \Yii::$app->has('session', true)
212 3
            && !is_null(\Yii::$app->getSession())
213 3
            && \Yii::$app->getSession()->getIsActive()
214 3
        ) {
215 3
            $attachment['fields'][] = [
216 3
                'title' => 'Session ID',
217 3
                'value' => '`' . $this->encodeMessage(\Yii::$app->getSession()->getId()) . '`',
218 3
                'short' => true,
219
            ];
220 3
        }
221 3
    }
222
223
    /**
224
     * Inserts traces into the attachement if applicable.
225
     *
226
     * @param array $traces
227
     * @param array $attachment
228
     */
229
    private function insertTracesIntoAttachment(array $traces, array &$attachment)
230
    {
231 1
        $traces = array_map(function ($trace) {
232 1
            return "in {$trace['file']}:{$trace['line']}";
233 1
        }, $traces);
234 1
        $attachment['fields'][] = [
235 1
            'title' => 'Stack Trace',
236 1
            'value' => "```\n" . $this->encodeMessage(implode("\n", $traces)) . "\n```",
237 1
            'short' => false,
238
        ];
239 1
    }
240
241
    /**
242
     * Inserts user data into the attachement if applicable.
243
     *
244
     * @param array $attachment
245
     */
246 3
    private function insertUserIntoAttachment(array &$attachment)
247
    {
248 3
        if (\Yii::$app->has('user', true) && !is_null(\Yii::$app->getUser())) {
249 3
            $user = \Yii::$app->getUser()->getIdentity(false);
250 3
            if (isset($user)) {
251 3
                $attachment['fields'][] = [
252 3
                    'title' => 'User ID',
253 3
                    'value' => $this->encodeMessage($user->getId()),
254 3
                    'short' => true,
255
                ];
256 3
            }
257 3
        }
258 3
    }
259
260
    /**
261
     * Inserts request data into the attachement if applicable.
262
     *
263
     * @param array $attachment
264
     */
265 4
    private function insertRequestIntoAttachment(array &$attachment)
266
    {
267 4
        if (\Yii::$app->getRequest() instanceof Request) {
268 3
            $attachment['author_name'] = $attachment['author_link'] = Url::current([], true);
269 3
            $attachment['fields'][] = [
270 3
                'title' => 'User IP',
271 3
                'value' => \Yii::$app->getRequest()->getUserIP(),
272 3
                'short' => true,
273
            ];
274 3
        } else {
275 1
            if (isset($_SERVER['argv'])) {
276 1
                $params = $_SERVER['argv'];
277 1
            } else {
278 1
                $params = [];
279
            }
280 1
            $attachment['author_name'] = implode(' ', $params);
281
        }
282 4
    }
283
284
    /**
285
     * Copies the value to the payload if the value is set.
286
     *
287
     * @param array $payload
288
     * @param string $name
289
     * @param string $value
290
     */
291 2
    private function insertIntoPayload(array &$payload, $name, $value)
292
    {
293 2
        if (isset($value)) {
294 2
            $payload[$name] = $value;
295 2
        }
296 2
    }
297
}
298