Completed
Push — master ( 50c0a6...9b8855 )
by Russell
01:25 queued 10s
created

SentryLogger   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 6
dl 0
loc 307
rs 9.44
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A defaultEnv() 0 4 1
A defaultTags() 0 9 1
A defaultExtra() 0 6 1
A getPackageInfo() 0 18 5
A getRequestType() 0 7 3
A getPeakMemory() 0 6 1
A getUserAgent() 0 10 2
A getReqMethod() 0 10 2
A getSAPI() 0 4 1
A getAdaptor() 0 4 1
A factory() 0 26 3
A backtrace() 0 35 3
B get_ip() 0 35 8
A user_data() 0 12 5
1
<?php
2
3
/**
4
 * Class: SentryLogger.
5
 *
6
 * @author  Russell Michell 2017-2019 <[email protected]>
7
 * @package phptek/sentry
8
 */
9
10
namespace PhpTek\Sentry\Log;
11
12
use SilverStripe\Control\Director;
13
use SilverStripe\Control\Middleware\TrustedProxyMiddleware;
14
use SilverStripe\Core\Injector\Injector;
15
use SilverStripe\Dev\Backtrace;
16
use SilverStripe\Security\Member;
17
use SilverStripe\Security\Security;
18
use SilverStripe\Core\Config\Configurable;
19
use PhpTek\Sentry\Adaptor\SentryAdaptor;
20
21
/**
22
 * The SentryLogWriter class is a bridge between {@link SentryAdaptor} and
23
 * SilverStripe's use of Monolog.
24
 */
25
class SentryLogger
26
{
27
    use Configurable;
28
29
    /**
30
     * @var SentryAdaptor
31
     */
32
    public $adaptor = null;
33
34
    /**
35
     * Stipulates what gets shown in the Sentry UI, should some metric not be
36
     * available for any reason.
37
     *
38
     * @const string
39
     */
40
    const SLW_NOOP = 'Unavailable';
41
42
    /**
43
     * A static constructor as per {@link Zend_Log_FactoryInterface}.
44
     *
45
     * @param  array $config    An array of optional additional configuration for
46
     *                          passing custom information to Sentry. See the README
47
     *                          for more detail.
48
     * @return SentryLogger
49
     */
50
    public static function factory(array $config = []) : SentryLogger
51
    {
52
        $env = $config['env'] ?? [];
53
        $user = $config['user'] ?? [];
54
        $tags = $config['tags'] ?? [];
55
        $extra = $config['extra'] ?? [];
56
        $level = $config['level'] ?? [];
57
        $logger = Injector::inst()->create(static::class);
58
59
        // Set default environment
60
        $env = $env ?: $logger->defaultEnv();
61
        // Set any available user data
62
        $user = $user ?: [];
63
        // Set any available tags available in SS config
64
        $tags = array_merge($logger->defaultTags(), $tags);
65
        // Set any available additional (extra) data
66
        $extra = array_merge($logger->defaultExtra(), $extra);
67
68
        $logger->adaptor->setContext('env', $env);
69
        $logger->adaptor->setContext('tags', $tags);
70
        $logger->adaptor->setContext('extra', $extra);
71
        $logger->adaptor->setContext('user', $user);
72
        $logger->adaptor->setContext('level', $level);
73
74
        return $logger;
75
    }
76
77
    /**
78
     * @return SentryAdaptor
79
     */
80
    public function getAdaptor() : SentryAdaptor
81
    {
82
        return $this->adaptor;
83
    }
84
85
    /**
86
     * Returns a default environment when one isn't passed to the factory()
87
     * method.
88
     *
89
     * @return string
90
     */
91
    public function defaultEnv() : string
92
    {
93
        return Director::get_environment_type();
94
    }
95
96
    /**
97
     * Returns a default set of additional "tags" we wish to send to Sentry.
98
     * By default, Sentry reports on several mertrics, and we're already sending
99
     * {@link Member} data. But there are additional data that would be useful
100
     * for debugging via the Sentry UI.
101
     *
102
     * These data can augment that which is sent to Sentry at setup
103
     * time in _config.php. See the README for more detail.
104
     *
105
     * N.b. Tags can be used to group messages within the Sentry UI itself, so there
106
     * should only be "static" data being sent, not something that can drastically
107
     * or minutely change, such as memory usage for example.
108
     *
109
     * @return array
110
     */
111
    public function defaultTags() : array
112
    {
113
        return [
114
            'Request-Method'=> $this->getReqMethod(),
115
            'Request-Type'  => $this->getRequestType(),
116
            'SAPI'          => $this->getSAPI(),
117
            'SS-Version'    => $this->getPackageInfo('silverstripe/framework')
118
        ];
119
    }
120
121
    /**
122
     * Returns a default set of extra data to show upon selecting a message for
123
     * analysis in the Sentry UI. This can augment the data sent to Sentry at setup
124
     * time in _config.php as well as at runtime when calling SS_Log itself.
125
     * See the README for more detail.
126
     *
127
     * @return array
128
     */
129
    public function defaultExtra() : array
130
    {
131
        return [
132
            'Peak-Memory'   => $this->getPeakMemory()
133
        ];
134
    }
135
136
    /**
137
     * Return the version of $pkg taken from composer.lock.
138
     *
139
     * @param  string $pkg e.g. "silverstripe/framework"
140
     * @return string
141
     */
142
    public function getPackageInfo(string $pkg) : string
143
    {
144
        $lockFileJSON = BASE_PATH . '/composer.lock';
145
146
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
147
            return self::SLW_NOOP;
148
        }
149
150
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
151
152
        foreach ($lockFileData['packages'] as $package) {
153
            if ($package['name'] === $pkg) {
154
                return $package['version'];
155
            }
156
        }
157
158
        return self::SLW_NOOP;
159
    }
160
161
    /**
162
     * What sort of request is this? (A harder question to answer than you might
163
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
164
     *
165
     * @return string
166
     */
167
    public function getRequestType() : string
168
    {
169
        $isCLI = $this->getSAPI() !== 'cli';
170
        $isAjax = Director::is_ajax();
171
172
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
173
    }
174
175
    /**
176
     * Return peak memory usage.
177
     *
178
     * @return string
179
     */
180
    public function getPeakMemory() : string
181
    {
182
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
183
184
        return (string) round($peak, 2) . 'Mb';
185
    }
186
187
    /**
188
     * Basic User-Agent check and return.
189
     *
190
     * @return string
191
     */
192
    public function getUserAgent() : string
193
    {
194
        $ua = @$_SERVER['HTTP_USER_AGENT'];
195
196
        if (!empty($ua)) {
197
            return $ua;
198
        }
199
200
        return self::SLW_NOOP;
201
    }
202
203
    /**
204
     * Basic request method check and return.
205
     *
206
     * @return string
207
     */
208
    public function getReqMethod() : string
209
    {
210
        $method = @$_SERVER['REQUEST_METHOD'];
211
212
        if (!empty($method)) {
213
            return $method;
214
        }
215
216
        return self::SLW_NOOP;
217
    }
218
219
    /**
220
     * @return string
221
     */
222
    public function getSAPI() : string
223
    {
224
        return php_sapi_name();
225
    }
226
227
    /**
228
     * Returns the client IP address which originated this request.
229
     * Lifted and modified from SilverStripe 3's SS_HTTPRequest.
230
     *
231
     * @return string
232
     */
233
    public static function get_ip() : string
234
    {
235
        $headerOverrideIP = null;
236
237
        if (defined('TRUSTED_PROXY')) {
238
            $headers = (defined('SS_TRUSTED_PROXY_IP_HEADER')) ?
239
                [SS_TRUSTED_PROXY_IP_HEADER] :
240
                null;
241
242
            if (!$headers) {
243
                // Backwards compatible defaults
244
                $headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'];
245
            }
246
247
            foreach ($headers as $header) {
248
                if (!empty($_SERVER[$header])) {
249
                    $headerOverrideIP = $_SERVER[$header];
250
251
                    break;
252
                }
253
            }
254
        }
255
256
        $proxy = Injector::inst()->create(TrustedProxyMiddleware::class);
257
258
        if ($headerOverrideIP) {
259
            return $proxy->getIPFromHeaderValue($headerOverrideIP);
260
        }
261
262
        if (isset($_SERVER['REMOTE_ADDR'])) {
263
            return $_SERVER['REMOTE_ADDR'];
264
        }
265
266
        return '';
267
    }
268
269
    /**
270
     * Returns a default set of additional data specific to the user's part in
271
     * the request.
272
     *
273
     * @param  mixed Member|null $member
274
     * @return array
275
     */
276
    public static function user_data(Member $member = null) : array
277
    {
278
        if (!$member) {
279
            $member = Security::getCurrentUser();
280
        }
281
282
        return [
283
            'IPAddress' => self::get_ip() ?: self::SLW_NOOP,
284
            'ID'       => $member ? $member->getField('ID') : self::SLW_NOOP,
285
            'Email'    => $member ? $member->getField('Email') : self::SLW_NOOP,
286
        ];
287
    }
288
289
    /**
290
     * Manually extract or generate a suitable backtrace. This is especially useful
291
     * in non-exception reports such as those that use Sentry\Client::captureMessage().
292
     *
293
     * @param  array $record
294
     * @return array
295
     */
296
    public static function backtrace(array $record) : array
297
    {
298
        if (!empty($record['context']['trace'])) {
299
            // Provided trace
300
            $bt = $record['context']['trace'];
301
        } elseif (isset($record['context']['exception'])) {
302
            // Generate trace from exception
303
            $bt = $record['context']['exception']->getTrace();
304
        } else {
305
            // Failover: build custom trace
306
            $bt = debug_backtrace();
307
308
            // Push current line into context
309
            array_unshift($bt, [
310
                'file'     => $bt['file'] ?? 'N/A',
311
                'line'     => $bt['line'] ?? 'N/A',
312
                'function' => '',
313
                'class'    => '',
314
                'type'     => '',
315
                'args'     => [],
316
            ]);
317
        }
318
319
        // Regardless of where it came from, filter the exception
320
        return Backtrace::filter_backtrace($bt, [
321
            '',
322
            'Monolog\\Handler\\AbstractProcessingHandler->handle',
323
            'Monolog\\Logger->addRecord',
324
            'Monolog\\Logger->error',
325
            'Monolog\\Logger->log',
326
            'Monolog\\Logger->warn',
327
            'PhpTek\\Sentry\\Handler\\SentryHandler->write',
328
            'PhpTek\\Sentry\\Handler\\SentryHandler->backtrace',
329
        ]);
330
    }
331
}
332