Completed
Push — master ( 6330c5...cda249 )
by Russell
07:31
created

SentryLogWriter::getClient()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 23 and the first side effect is on line 12.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
/**
4
 * Class: SentryLogWriter.
5
 *
6
 * @author  Russell Michell 2017 <[email protected]>
7
 * @package phptek/sentry
8
 */
9
10
namespace SilverStripeSentry;
11
12
require_once THIRDPARTY_PATH . '/Zend/Log/Writer/Abstract.php';
13
14
/**
15
 * The SentryLogWriter class simply acts as a bridge between the configured Sentry 
16
 * adaptor and SilverStripe's {@link SS_Log}.
17
 * 
18
 * Usage in your project's _config.php for example (See README for examples).
19
 *  
20
 *    SS_Log::add_writer(\SilverStripeSentry\SentryLogWriter::factory(), '<=');
21
 */
22
23
class SentryLogWriter extends \Zend_Log_Writer_Abstract
24
{
25
    
26
    /**
27
     * Stipulates what gets shown in the Sentry UI, should some metric not be
28
     * available for any reason.
29
     *
30
     * @const string
31
     */
32
    const SLW_NOOP = 'Unavailable';
33
    
34
    /**
35
     * For flexibility, the factory should be the usual entry point into this class,
36
     * but there's no reason the constructor can't be called directly if for example, only
37
     * local errror-reporting is required.
38
     * 
39
     * @param  array $config
40
     * @return SentryLogWriter
41
     */
42
    public static function factory($config = [])
43
    {
44
        $env = isset($config['env']) ? $config['env'] : null;
45
        $tags = isset($config['tags']) ? $config['tags'] : [];
46
        $extra = isset($config['extra']) ? $config['extra'] : [];
47
48
        $writer = \Injector::inst()->get('SentryLogWriter');
49
50
        // Set default environment
51
        if (is_null($env)) {
52
            $env = \Director::get_environment_type();
53
        }
54
55
        // Set all available user-data
56
        $userData = $writer->defaultUserData();
57
        if ($member = \Member::currentUser()) {
58
            $userData = $writer->defaultUserData($member);
59
        }
60
61
        // Set any available tags available in SS config
62
        $tags = array_merge($writer->defaultTags(), $tags);
63
64
        // Set any avalable additional (extra) data
65
        $extra = array_merge($writer->defaultExtra(), $extra);
66
67
        $writer->client->setData('env', $env);
68
        $writer->client->setData('user', $userData);
69
        $writer->client->setData('tags', $tags);
70
        $writer->client->setData('extra', $extra);
71
72
        return $writer;
73
    }
74
75
    public function getClient()
76
    {
77
        return $this->client;
78
    }
79
    
80
    /**
81
     * Returns a default set of additional data specific to the user's part in
82
     * the request.
83
     * 
84
     * @param  Member $member
85
     * @return array
86
     */
87
    public function defaultUserData(\Member $member = null)
88
    {
89
        return [
90
            'IP-Address'    => $this->getIP(),
91
            'ID'            => $member ? $member->getField('ID') : self::SLW_NOOP,
92
            'Email'         => $member ? $member->getField('Email') : self::SLW_NOOP,
93
        ];
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
     * N.b. Tags can be used to group messages within the Sentry UI itself, so you
103
     * only want "static" data, not somethng that can drastically or minutely change,
104
     * such as memory usage for example.
105
     * 
106
     * @return array
107
     */
108
    public function defaultTags()
109
    {
110
        return [
111
            'Request-Method'=> $this->getReqMethod(),
112
            'Request-Type'  => $this->getRequestType(),
113
            'SAPI'          => $this->getSAPI(),
114
            'SS-Version'    => $this->getPackageInfo('silverstripe/framework')
115
        ];
116
    }
117
118
    /**
119
     * Returns a default set of extra data to show upon selecting a message for analysis
120
     * in the Sentry UI.
121
     *
122
     * @return array
123
     */
124
    public function defaultExtra()
125
    {
126
        return [
127
            'Peak-Memory'   => $this->getPeakMemory()
128
        ];
129
    }
130
    
131
    /**
132
     * _write() forms the entry point into the physical sending of the error. The 
133
     * sending itself is done by the current client's `send()` method.
134
     * 
135
     * @param  array $event An array of data that is created in, and arrives here via {@link SS_Log::log()} and {@link Zend_Log::log}.
136
     *                      via {@link SS_Log::log()} and {@link Zend_Log::log}.
137
     * @return void
138
     */
139
    protected function _write($event)
140
    {
141
        $message = $event['message']['errstr'];                             // From SS_Log::log()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
142
        // The complete compliment of these data come via the Raven_Client::xxx_context() methods
143
        $data = [
144
            'timestamp' => strtotime($event['timestamp']),                  // From Zend_Log::log()
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
145
            'extra'     => isset($event['extra']) ? $event['extra'] : ''    // From _config.php (Optional)
146
        ];
147
        $trace = \SS_Backtrace::filter_backtrace(debug_backtrace(), ['SentryLogWriter->_write']);
148
        
149
        $this->client->send($message, [], $data, $trace);
150
    }
151
    
152
    /**
153
     * Return the version of $pkg taken from composer.lock.
154
     * 
155
     * @param  string $pkg e.g. "silverstripe/framework"
156
     * @return string
157
     */
158
    public function getPackageInfo($pkg)
159
    {
160
        $lockFileJSON = BASE_PATH . '/composer.lock';
161
162
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
163
            return self::SLW_NOOP;
164
        }
165
166
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
167
168
        foreach ($lockFileData['packages'] as $package) {
169
            if ($package['name'] === $pkg) {
170
                return $package['version'];
171
            }
172
        }
173
        
174
        return self::SLW_NOOP;
175
    }
176
    
177
    /**
178
     * Return the IP address of the relevant request.
179
     * 
180
     * @return string
181
     */
182
    public function getIP()
183
    {
184
        $req = \Injector::inst()->create('SS_HTTPRequest', $this->getReqMethod(), '');
185
        if ($ip = $req->getIP()) {
186
            return $ip;
187
        }
188
        
189
        return self::SLW_NOOP;
190
    }
191
    
192
    /**
193
     * What sort of request is this? (A harder question to answer than you might
194
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
195
     * 
196
     * @return string
197
     */
198
    public function getRequestType()
199
    {
200
        $isCLI = $this->getSAPI() !== 'cli';
201
        $isAjax = \Director::is_ajax();
202
203
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
204
    }
205
    
206
    /**
207
     * Return peak memory usage.
208
     * 
209
     * @return float
210
     */
211
    public function getPeakMemory()
212
    {
213
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
214
        
215
        return (string) round($peak, 2) . 'Mb';
216
    }
217
    
218
    /**
219
     * Basic User-Agent check and return.
220
     * 
221
     * @return string
222
     */
223
    public function getUserAgent()
0 ignored issues
show
Coding Style introduced by
getUserAgent uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
224
    {
225
        if (!empty($ua = @$_SERVER['HTTP_USER_AGENT'])) {
226
            return $ua;
227
        }
228
        
229
        return self::SLW_NOOP;
230
    }
231
    
232
    /**
233
     * Basic reuqest method check and return.
234
     * 
235
     * @return string
236
     */
237
    public function getReqMethod()
0 ignored issues
show
Coding Style introduced by
getReqMethod uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
238
    {
239
        if (!empty($method = @$_SERVER['REQUEST_METHOD'])) {
240
            return $method;
241
        }
242
        
243
        return self::SLW_NOOP;
244
    }
245
    
246
    /**
247
     * @return string
248
     */
249
    public function getSAPI()
250
    {
251
        return php_sapi_name();
252
    }
253
    
254
}
255