Completed
Pull Request — master (#40)
by Simon
01:37
created

SentryLogger::defaultUser()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.5555
c 0
b 0
f 0
cc 5
nc 16
nop 1
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\Security;
17
use SilverStripe\Core\Config\Configurable;
18
use PhpTek\Sentry\Adaptor\SentryAdaptor;
19
20
/**
21
 * The SentryLogWriter class is a bridge between {@link SentryAdaptor} and
22
 * SilverStripe's use of Monolog.
23
 */
24
class SentryLogger
25
{
26
    use Configurable;
27
28
    /**
29
     * @var SentryAdaptor
30
     */
31
    public $client = null;
32
33
    /**
34
     * Stipulates what gets shown in the Sentry UI, should some metric not be
35
     * available for any reason.
36
     *
37
     * @const string
38
     */
39
    const SLW_NOOP = 'Unavailable';
40
41
    /**
42
     * A static constructor as per {@link Zend_Log_FactoryInterface}.
43
     *
44
     * @param array $config An array of optional additional configuration for
45
     *                          passing custom information to Sentry. See the README
46
     *                          for more detail.
47
     * @return SentryLogger
48
     */
49
    public static function factory(array $config = []): SentryLogger
50
    {
51
        $env = $config['env'] ?? [];
52
        $user = $config['user'] ?? [];
53
        $tags = $config['tags'] ?? [];
54
        $extra = $config['extra'] ?? [];
55
        // Set the minimum reporting level
56
        $level = $config['level'] ?? self::config()->get('log_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 ?: $logger->defaultUser();
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;
0 ignored issues
show
Bug introduced by
The property adaptor does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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 = $this->getGitInfo();
132
        $return['Peak-Memory'] = $this->getPeakMemory();
133
134
        return $return;
135
    }
136
137
138
    /**
139
     * Return the version of $pkg taken from composer.lock.
140
     *
141
     * @param string $pkg e.g. "silverstripe/framework"
142
     * @return string
143
     */
144
    public function getPackageInfo(string $pkg): string
145
    {
146
        $lockFileJSON = BASE_PATH . '/composer.lock';
147
148
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
149
            return self::SLW_NOOP;
150
        }
151
152
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
153
154
        foreach ($lockFileData['packages'] as $package) {
155
            if ($package['name'] === $pkg) {
156
                return $package['version'];
157
            }
158
        }
159
160
        return self::SLW_NOOP;
161
    }
162
163
    /**
164
     * What sort of request is this? (A harder question to answer than you might
165
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
166
     *
167
     * @return string
168
     */
169
    public function getRequestType(): string
170
    {
171
        $isCLI = $this->getSAPI() !== 'cli';
172
        $isAjax = Director::is_ajax();
173
174
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
175
    }
176
177
    /**
178
     * Return peak memory usage.
179
     *
180
     * @return string
181
     */
182
    public function getPeakMemory(): string
183
    {
184
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
185
186
        return (string)round($peak, 2) . 'Mb';
187
    }
188
189
    /**
190
     * Basic User-Agent check and return.
191
     *
192
     * @return string
193
     */
194
    public function getUserAgent(): string
195
    {
196
        $ua = @$_SERVER['HTTP_USER_AGENT'];
197
198
        if (!empty($ua)) {
199
            return $ua;
200
        }
201
202
        return self::SLW_NOOP;
203
    }
204
205
    /**
206
     * Basic request method check and return.
207
     *
208
     * @return string
209
     */
210
    public function getReqMethod(): string
211
    {
212
        $method = @$_SERVER['REQUEST_METHOD'];
213
214
        if (!empty($method)) {
215
            return $method;
216
        }
217
218
        return self::SLW_NOOP;
219
    }
220
221
    /**
222
     * @return string
223
     */
224
    public function getSAPI(): string
225
    {
226
        return php_sapi_name();
227
    }
228
229
    /**
230
     * Returns the client IP address which originated this request.
231
     * Lifted and modified from SilverStripe 3's SS_HTTPRequest.
232
     *
233
     * @return string
234
     */
235
    public function getIP(): string
236
    {
237
        $headerOverrideIP = null;
238
239
        if (defined('TRUSTED_PROXY')) {
240
            $headers = (defined('SS_TRUSTED_PROXY_IP_HEADER')) ?
241
                [SS_TRUSTED_PROXY_IP_HEADER] :
242
                null;
243
244
            if (!$headers) {
245
                // Backwards compatible defaults
246
                $headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'];
247
            }
248
249
            foreach ($headers as $header) {
250
                if (!empty($_SERVER[$header])) {
251
                    $headerOverrideIP = $_SERVER[$header];
252
253
                    break;
254
                }
255
            }
256
        }
257
258
        $proxy = Injector::inst()->create(TrustedProxyMiddleware::class);
259
260
        if ($headerOverrideIP) {
261
            return $proxy->getIPFromHeaderValue($headerOverrideIP);
262
        }
263
264
        if (isset($_SERVER['REMOTE_ADDR'])) {
265
            return $_SERVER['REMOTE_ADDR'];
266
        }
267
268
        return '';
269
    }
270
271
    /**
272
     * Returns a default set of additional data specific to the user's part in
273
     * the request.
274
     *
275
     * @param mixed Member|null $member
276
     * @return array
277
     */
278
    public function defaultUser(Member $member = null): array
279
    {
280
        if (!$member) {
281
            $member = Security::getCurrentUser();
282
        }
283
284
        return [
285
            'IPAddress' => $this->getIP() ?: self::SLW_NOOP,
286
            'ID'        => $member ? $member->getField('ID') : self::SLW_NOOP,
287
            'Email'     => $member ? $member->getField('Email') : self::SLW_NOOP,
288
        ];
289
    }
290
291
    /**
292
     * Generate a cleaned-up backtrace of the event that got us here.
293
     *
294
     * @param array $record
295
     * @return array
296
     * @todo   Unused in sentry-sdk 2.0??
297
     */
298
    public static function backtrace(array $record): array
299
    {
300
        // Provided trace
301
        if (!empty($record['context']['trace'])) {
302
            return $record['context']['trace'];
303
        }
304
305
        // Generate trace from exception
306
        if (isset($record['context']['exception'])) {
307
            $exception = $record['context']['exception'];
308
309
            return $exception->getTrace();
310
        }
311
312
        // Failover: build custom trace
313
        $bt = debug_backtrace();
314
315
        // Push current line into context
316
        array_unshift($bt, [
317
            'file'     => !empty($bt['file']) ? $bt['file'] : 'N/A',
318
            'line'     => !empty($bt['line']) ? $bt['line'] : 'N/A',
319
            'function' => '',
320
            'class'    => '',
321
            'type'     => '',
322
            'args'     => [],
323
        ]);
324
325
        return Backtrace::filter_backtrace($bt, [
326
            '',
327
            'Monolog\\Handler\\AbstractProcessingHandler->handle',
328
            'Monolog\\Logger->addRecord',
329
            'Monolog\\Logger->log',
330
            'Monolog\\Logger->warn',
331
            'PhpTek\\Sentry\\Handler\\SentryMonologHandler->write',
332
            'PhpTek\\Sentry\\Handler\\SentryMonologHandler->backtrace',
333
        ]);
334
    }
335
336
    /**
337
     * Get the information about the current release, if possible
338
     * @return array
339
     */
340
    protected function getGitInfo()
341
    {
342
        // Initialise empty array to return
343
        $return = [];
344
        // If the file operations error out, we need to catch it
345
        try {
346
            $return = $this->getGitCommitMessage($return);
347
        } catch (\Exception $exception) {
348
            // Default message. As it's an extra, it's not overly crowding the interface
349
            $return['Message'] = 'No git repo found or inaccessible';
350
        }
351
352
        return $return;
353
    }
354
355
    /**
356
     * Get the HEAD tag or the release
357
     *
358
     * @return string
359
     */
360
    public static function getGitHead()
361
    {
362
        $head = file_exists(Director::baseFolder() . '/.git/HEAD');
363
        if ($head) {
364
            $head = explode(':', file_get_contents(Director::baseFolder() . '/.git/HEAD'));
365
            $ref = file_get_contents(Director::baseFolder() . '/.git/' . trim($head[1]));
366
            $ref = self::getGitTag($ref);
367
            return $ref;
368
        }
369
370
        return '';
371
    }
372
373
    /**
374
     * Check if we are on a tagged release and if so, use that to set the client release
375
     * @param $ref
376
     * @return mixed
377
     */
378
    protected static function getGitTag($ref)
379
    {
380
        $data = file_get_contents(Director::baseFolder() . './git/packed-refs');
381
        if ($data) {
382
            $data = explode('\n', $data);
383
            foreach ($data as $line) {
384
                if (strpos($line, '#') !== false) {
385
                    continue;
386
                }
387
                $tags = explode(' ', $line);
388
                if ($tags[0] === $ref) {
389
                    list(,,$tag) = explode('/', $ref[1]);
390
                    return $tag;
391
                }
392
            }
393
        }
394
395
        return $ref;
396
    }
397
398
    /**
399
     * @param array $return
400
     * @return array
401
     */
402
    protected function getGitCommitMessage(array $return): array
403
    {
404
        $msgFile = file_exists(Director::baseFolder() . '/.git/COMMIT_EDITMSG');
405
        if ($msgFile) {
406
            $return['Message'] = file_get_contents(Director::baseFolder() . '/.git/COMMIT_EDITMSG');
407
        }
408
409
        return $return;
410
    }
411
412
}
413