Completed
Pull Request — master (#40)
by Simon
02:08 queued 41s
created

SentryLogger::getGitHead()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
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 = [];
0 ignored issues
show
Unused Code introduced by
$return is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
344
        // If the file operations error out, we need to catch it
345
        try {
346
            $return = [];
347
            $return = $this->getGitCommitMessage($return);
348
        } catch (\Exception $exception) {
349
            // Default message. As it's an extra, it's not overly crowding the interface
350
            $return['Message'] = 'No git repo found or inaccessible';
351
        }
352
353
        return $return;
354
    }
355
356
    /**
357
     * Get the HEAD tag or the release
358
     * @param SentryAdaptor $client
359
     */
360
    public static function getGitHead($client)
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
            $client->getOptions()->setRelease(trim($ref));
0 ignored issues
show
Bug introduced by
The method getOptions() does not seem to exist on object<PhpTek\Sentry\Adaptor\SentryAdaptor>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
368
        }
369
    }
370
371
    /**
372
     * Check if we are on a tagged release and if so, use that to set the client release
373
     * @param $ref
374
     * @return mixed
375
     */
376
    protected static function getGitTag($ref)
377
    {
378
        $data = file_get_contents(Director::baseFolder() . './git/packed-refs');
379
        if ($data) {
380
            $data = explode('\n', $data);
381
            foreach ($data as $line) {
382
                if (strpos($line, '#') !== false) {
383
                    continue;
384
                }
385
                $tags = explode(' ', $line);
386
                if ($tags[0] === $ref) {
387
                    list($ref, $tags, $tag) = explode('/', $ref);
0 ignored issues
show
Unused Code introduced by
The assignment to $ref is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
Unused Code introduced by
The assignment to $tags is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
388
                    return $tag;
389
                }
390
            }
391
        }
392
393
        return $ref;
394
    }
395
396
    /**
397
     * @param array $return
398
     * @return array
399
     */
400
    protected function getGitCommitMessage(array $return): array
401
    {
402
        $msgFile = file_exists(Director::baseFolder() . '/.git/COMMIT_EDITMSG');
403
        if ($msgFile) {
404
            $return['Message'] = file_get_contents(Director::baseFolder() . '/.git/COMMIT_EDITMSG');
405
        }
406
407
        return $return;
408
    }
409
410
}
411