Completed
Push — master ( ca12f9...6330c5 )
by Russell
02:03
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 silverstripe/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
    /**
76
     * Returns a default set of additional data specific to the user's part in
77
     * the request.
78
     * 
79
     * @param  Member $member
80
     * @return array
81
     */
82
    public function defaultUserData(\Member $member = null)
83
    {
84
        return [
85
            'IP-Address'    => $this->getIP(),
86
            'ID'            => $member ? $member->getField('ID') : self::SLW_NOOP,
87
            'Email'         => $member ? $member->getField('Email') : self::SLW_NOOP,
88
        ];
89
    }
90
    
91
    /**
92
     * Returns a default set of additional tags we wish to send to Sentry.
93
     * By default, Sentry reports on several mertrics, and we're already sending 
94
     * {@link Member} data. But there are additional data that would be useful
95
     * for debugging via the Sentry UI.
96
     *
97
     * N.b. Tags can be used to group messages within the Sentry UI itself, so you
98
     * only want "static" data, not somethng that can drastically or minutely change,
99
     * such as memory usage for example.
100
     * 
101
     * @return array
102
     */
103
    public function defaultTags()
104
    {
105
        return [
106
            'Request-Method'=> $this->getReqMethod(),
107
            'Request-Type'  => $this->getRequestType(),
108
            'SAPI'          => $this->getSAPI(),
109
            'SS-Version'    => $this->getPackageInfo('silverstripe/framework')
110
        ];
111
    }
112
113
    /**
114
     * Returns a default set of extra data to show upon selecting a message for analysis
115
     * in the Sentry UI.
116
     *
117
     * @return array
118
     */
119
    public function defaultExtra()
120
    {
121
        return [
122
            'Peak-Memory'   => $this->getPeakMemory()
123
        ];
124
    }
125
    
126
    /**
127
     * _write() forms the entry point into the physical sending of the error. The 
128
     * sending itself is done by the current client's `send()` method.
129
     * 
130
     * @param  array $event An array of data that is created in, and arrives here via {@link SS_Log::log()} and {@link Zend_Log::log}.
131
     *                      via {@link SS_Log::log()} and {@link Zend_Log::log}.
132
     * @return void
133
     */
134
    protected function _write($event)
135
    {
136
        $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...
137
        // The complete compliment of these data come via the Raven_Client::xxx_context() methods
138
        $data = [
139
            '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...
140
            'extra'     => isset($event['extra']) ? $event['extra'] : ''    // From _config.php (Optional)
141
        ];
142
        $trace = \SS_Backtrace::filter_backtrace(debug_backtrace(), ['SentryLogWriter->_write']);
143
        
144
        $this->client->send($message, [], $data, $trace);
145
    }
146
    
147
    /**
148
     * Return the version of $pkg taken from composer.lock.
149
     * 
150
     * @param  string $pkg e.g. "silverstripe/framework"
151
     * @return string
152
     */
153
    public function getPackageInfo($pkg)
154
    {
155
        $lockFileJSON = BASE_PATH . '/composer.lock';
156
157
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
158
            return self::SLW_NOOP;
159
        }
160
161
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
162
163
        foreach ($lockFileData['packages'] as $package) {
164
            if ($package['name'] === $pkg) {
165
                return $package['version'];
166
            }
167
        }
168
        
169
        return self::SLW_NOOP;
170
    }
171
    
172
    /**
173
     * Return the IP address of the relevant request.
174
     * 
175
     * @return string
176
     */
177
    public function getIP()
178
    {
179
        $req = \Injector::inst()->create('SS_HTTPRequest', $this->getReqMethod(), '');
180
        if ($ip = $req->getIP()) {
181
            return $ip;
182
        }
183
        
184
        return self::SLW_NOOP;
185
    }
186
    
187
    /**
188
     * What sort of request is this? (A harder question to answer than you might
189
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
190
     * 
191
     * @return string
192
     */
193
    public function getRequestType()
194
    {
195
        $isCLI = $this->getSAPI() !== 'cli';
196
        $isAjax = \Director::is_ajax();
197
198
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
199
    }
200
    
201
    /**
202
     * Return peak memory usage.
203
     * 
204
     * @return float
205
     */
206
    public function getPeakMemory()
207
    {
208
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
209
        
210
        return (string) round($peak, 2) . 'Mb';
211
    }
212
    
213
    /**
214
     * Basic User-Agent check and return.
215
     * 
216
     * @return string
217
     */
218
    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...
219
    {
220
        if (!empty($ua = @$_SERVER['HTTP_USER_AGENT'])) {
221
            return $ua;
222
        }
223
        
224
        return self::SLW_NOOP;
225
    }
226
    
227
    /**
228
     * Basic reuqest method check and return.
229
     * 
230
     * @return string
231
     */
232
    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...
233
    {
234
        if (!empty($method = @$_SERVER['REQUEST_METHOD'])) {
235
            return $method;
236
        }
237
        
238
        return self::SLW_NOOP;
239
    }
240
    
241
    /**
242
     * @return string
243
     */
244
    public function getSAPI()
245
    {
246
        return php_sapi_name();
247
    }
248
    
249
}
250