Completed
Push — master ( e44348...b1141f )
by Russell
03:58
created

src/Log/SentryLogger.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Log\SentryLogger;
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 $client = 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
        // Set the minimum reporting level
57
        $level = $config['level'] ?? self::config()->get('log_level');
58
        $logger = Injector::inst()->create(static::class);
59
60
        // Set default environment
61
        $env = $env ?: $logger->defaultEnv();
62
        // Set any available user data
63
        $user = $user ?: $logger->defaultUser();
64
        // Set any available tags available in SS config
65
        $tags = array_merge($logger->defaultTags(), $tags);
66
        // Set any available additional (extra) data
67
        $extra = array_merge($logger->defaultExtra(), $extra);
68
69
        $logger->adaptor->setContext('env', $env);
70
        $logger->adaptor->setContext('tags', $tags);
71
        $logger->adaptor->setContext('extra', $extra);
72
        $logger->adaptor->setContext('user', $user);
73
        $logger->adaptor->setContext('level', $level);
74
75
        return $logger;
76
    }
77
78
    /**
79
     * @return SentryAdaptor
80
     */
81
    public function getAdaptor() : SentryAdaptor
82
    {
83
        return $this->adaptor;
84
    }
85
86
    /**
87
     * Returns a default environment when one isn't passed to the factory()
88
     * method.
89
     *
90
     * @return string
91
     */
92
    public function defaultEnv() : string
93
    {
94
        return Director::get_environment_type();
95
    }
96
97
    /**
98
     * Returns a default set of additional "tags" we wish to send to Sentry.
99
     * By default, Sentry reports on several mertrics, and we're already sending
100
     * {@link Member} data. But there are additional data that would be useful
101
     * for debugging via the Sentry UI.
102
     *
103
     * These data can augment that which is sent to Sentry at setup
104
     * time in _config.php. See the README for more detail.
105
     *
106
     * N.b. Tags can be used to group messages within the Sentry UI itself, so there
107
     * should only be "static" data being sent, not something that can drastically
108
     * or minutely change, such as memory usage for example.
109
     *
110
     * @return array
111
     */
112
    public function defaultTags() : array
113
    {
114
        return [
115
            'Request-Method'=> $this->getReqMethod(),
116
            'Request-Type'  => $this->getRequestType(),
117
            'SAPI'          => $this->getSAPI(),
118
            'SS-Version'    => $this->getPackageInfo('silverstripe/framework')
119
        ];
120
    }
121
122
    /**
123
     * Returns a default set of extra data to show upon selecting a message for
124
     * analysis in the Sentry UI. This can augment the data sent to Sentry at setup
125
     * time in _config.php as well as at runtime when calling SS_Log itself.
126
     * See the README for more detail.
127
     *
128
     * @return array
129
     */
130
    public function defaultExtra() : array
131
    {
132
        return [
133
            'Peak-Memory'   => $this->getPeakMemory()
134
        ];
135
    }
136
137
    /**
138
     * Return the version of $pkg taken from composer.lock.
139
     *
140
     * @param  string $pkg e.g. "silverstripe/framework"
141
     * @return string
142
     */
143
    public function getPackageInfo(string $pkg) : string
144
    {
145
        $lockFileJSON = BASE_PATH . '/composer.lock';
146
147
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
148
            return self::SLW_NOOP;
149
        }
150
151
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
152
153
        foreach ($lockFileData['packages'] as $package) {
154
            if ($package['name'] === $pkg) {
155
                return $package['version'];
156
            }
157
        }
158
159
        return self::SLW_NOOP;
160
    }
161
162
    /**
163
     * What sort of request is this? (A harder question to answer than you might
164
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
165
     *
166
     * @return string
167
     */
168
    public function getRequestType() : string
169
    {
170
        $isCLI = $this->getSAPI() !== 'cli';
171
        $isAjax = Director::is_ajax();
172
173
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
174
    }
175
176
    /**
177
     * Return peak memory usage.
178
     *
179
     * @return string
180
     */
181
    public function getPeakMemory() : string
182
    {
183
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
184
185
        return (string) round($peak, 2) . 'Mb';
186
    }
187
188
    /**
189
     * Basic User-Agent check and return.
190
     *
191
     * @return string
192
     */
193
    public function getUserAgent() : string
194
    {
195
        $ua = @$_SERVER['HTTP_USER_AGENT'];
196
197
        if (!empty($ua)) {
198
            return $ua;
199
        }
200
201
        return self::SLW_NOOP;
202
    }
203
204
    /**
205
     * Basic request method check and return.
206
     *
207
     * @return string
208
     */
209
    public function getReqMethod() : string
210
    {
211
        $method = @$_SERVER['REQUEST_METHOD'];
212
213
        if (!empty($method)) {
214
            return $method;
215
        }
216
217
        return self::SLW_NOOP;
218
    }
219
220
    /**
221
     * @return string
222
     */
223
    public function getSAPI() : string
224
    {
225
        return php_sapi_name();
226
    }
227
228
 	/**
229
	 * Returns the client IP address which originated this request.
230
     * Lifted and modified from SilverStripe 3's SS_HTTPRequest.
231
	 *
232
	 * @return string
233
	 */
234
	public function getIP() : string
235
    {
236
		$headerOverrideIP = null;
237
238
		if (defined('TRUSTED_PROXY')) {
239
			$headers = (defined('SS_TRUSTED_PROXY_IP_HEADER')) ?
240
                [SS_TRUSTED_PROXY_IP_HEADER] :
241
                null;
242
243
			if(!$headers) {
244
				// Backwards compatible defaults
245
				$headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'];
246
			}
247
248
			foreach($headers as $header) {
249
				if(!empty($_SERVER[$header])) {
250
					$headerOverrideIP = $_SERVER[$header];
251
252
					break;
253
				}
254
			}
255
		}
256
257
        $proxy = Injector::inst()->create(TrustedProxyMiddleware::class);
258
259
		if ($headerOverrideIP) {
260
			return $proxy->getIPFromHeaderValue($headerOverrideIP);
261
		}
262
263
        if (isset($_SERVER['REMOTE_ADDR'])) {
264
			return $_SERVER['REMOTE_ADDR'];
265
		}
266
267
        return '';
268
	}
269
270
    /**
271
     * Returns a default set of additional data specific to the user's part in
272
     * the request.
273
     *
274
     * @param  mixed Member|null $member
275
     * @return array
276
     */
277 View Code Duplication
    public function defaultUser(Member $member = null) : array
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
278
    {
279
        if (!$member) {
280
            $member = Security::getCurrentUser();
281
        }
282
        
283
        return [
284
            'IPAddress' => $this->getIP() ?: self::SLW_NOOP,
285
            'ID'       => $member ? $member->getField('ID') : self::SLW_NOOP,
286
            'Email'    => $member ? $member->getField('Email') : self::SLW_NOOP,
287
        ];
288
    }
289
290
    /**
291
     * Generate a cleaned-up backtrace of the event that got us here.
292
     *
293
     * @param  array $record
294
     * @return array
295
     * @todo   Unused in sentry-sdk 2.0??
296
     */
297 View Code Duplication
    public static function backtrace(array $record) : array
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
298
    {
299
        // Provided trace
300
        if (!empty($record['context']['trace'])) {
301
            return $record['context']['trace'];
302
        }
303
304
        // Generate trace from exception
305
        if (isset($record['context']['exception'])) {
306
            $exception = $record['context']['exception'];
307
308
            return $exception->getTrace();
309
        }
310
311
        // Failover: build custom trace
312
        $bt = debug_backtrace();
313
314
        // Push current line into context
315
        array_unshift($bt, [
316
            'file'     => !empty($bt['file']) ? $bt['file'] : 'N/A',
317
            'line'     => !empty($bt['line']) ? $bt['line'] : 'N/A',
318
            'function' => '',
319
            'class'    => '',
320
            'type'     => '',
321
            'args'     => [],
322
        ]);
323
324
       return Backtrace::filter_backtrace($bt, [
325
            '',
326
            'Monolog\\Handler\\AbstractProcessingHandler->handle',
327
            'Monolog\\Logger->addRecord',
328
            'Monolog\\Logger->log',
329
            'Monolog\\Logger->warn',
330
            'PhpTek\\Sentry\\Handler\\SentryMonologHandler->write',
331
            'PhpTek\\Sentry\\Handler\\SentryMonologHandler->backtrace',
332
        ]);
333
    }
334
335
}
336