Completed
Push — master ( a10ad6...262a1e )
by Russell
20:23
created

SentryLogger::getPackageInfo()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 4
nop 1
1
<?php
2
3
/**
4
 * Class: SentryLogWriter.
5
 *
6
 * @author  Russell Michell 2017 <[email protected]>
7
 * @package phptek/sentry
8
 */
9
10
namespace PhpTek\Sentry\Log;
11
12
use SilverStripe\Control\Director,
13
    SilverStripe\Core\Injector\Injector;
14
15
/**
16
 * The SentryLogWriter class simply acts as a bridge between the configured Sentry 
17
 * adaptor and SilverStripe's {@link SS_Log}.
18
 * 
19
 * Usage in your project's _config.php for example (See README for examples).
20
 *  
21
 *    SS_Log::add_writer(\phptek\Sentry\SentryLogWriter::factory(), '<=');
22
 */
23
24
class SentryLogger
25
{
26
    
27
    /**
28
     * Stipulates what gets shown in the Sentry UI, should some metric not be
29
     * available for any reason.
30
     *
31
     * @const string
32
     */
33
    const SLW_NOOP = 'Unavailable';
34
    
35
    /**
36
     * A static constructor as per {@link Zend_Log_FactoryInterface}.
37
     * 
38
     * @param  array $config    An array of optional additional configuration for
39
     *                          passing custom information to Sentry. See the README
40
     *                          for more detail.
41
     * @return SentryLogger
42
     */
43
    public static function factory($config = [])
44
    {
45
        $env = isset($config['env']) ? $config['env'] : null;
46
        $tags = isset($config['tags']) ? $config['tags'] : [];
47
        $extra = isset($config['extra']) ? $config['extra'] : [];
48
        $logger = Injector::inst()->create(__CLASS__);
49
50
        // Set default environment
51
        if (is_null($env)) {
52
            $env = $logger->defaultEnv();
53
        }
54
55
        // Set any available tags available in SS config
56
        $tags = array_merge($logger->defaultTags(), $tags);
57
58
        // Set any available additional (extra) data
59
        $extra = array_merge($logger->defaultExtra(), $extra);
60
61
        $logger->client->setData('env', $env);
62
        $logger->client->setData('tags', $tags);
63
        $logger->client->setData('extra', $extra);
64
        
65
        return $logger;
66
    }
67
68
    /**
69
     * Used in unit tests.
70
     *
71
     * @return SentryClientAdaptor
72
     */
73
    public function getClient()
74
    {
75
        return $this->client;
0 ignored issues
show
Bug introduced by
The property client 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...
76
    }
77
78
    /**
79
     * Returns a default environment when one isn't passed to the factory()
80
     * method.
81
     *
82
     * @return string
83
     */
84
    public function defaultEnv()
85
    {
86
        return Director::get_environment_type();
87
    }
88
    
89
    /**
90
     * Returns a default set of additional "tags" we wish to send to Sentry.
91
     * By default, Sentry reports on several mertrics, and we're already sending 
92
     * {@link Member} data. But there are additional data that would be useful
93
     * for debugging via the Sentry UI.
94
     *
95
     * These data can augment that which is sent to Sentry at setup
96
     * time in _config.php. See the README for more detail.
97
     *
98
     * N.b. Tags can be used to group messages within the Sentry UI itself, so there
99
     * should only be "static" data being sent, not something that can drastically
100
     * or minutely change, such as memory usage for example.
101
     * 
102
     * @return array
103
     */
104
    public function defaultTags()
105
    {
106
        return [
107
            'Request-Method'=> $this->getReqMethod(),
108
            'Request-Type'  => $this->getRequestType(),
109
            'SAPI'          => $this->getSAPI(),
110
            'SS-Version'    => $this->getPackageInfo('silverstripe/framework')
111
        ];
112
    }
113
114
    /**
115
     * Returns a default set of extra data to show upon selecting a message for
116
     * analysis in the Sentry UI. This can augment the data sent to Sentry at setup
117
     * time in _config.php as well as at runtime when calling SS_Log itself.
118
     * See the README for more detail.
119
     *
120
     * @return array
121
     */
122
    public function defaultExtra()
123
    {
124
        return [
125
            'Peak-Memory'   => $this->getPeakMemory()
126
        ];
127
    }
128
    
129
    /**
130
     * Return the version of $pkg taken from composer.lock.
131
     * 
132
     * @param  string $pkg e.g. "silverstripe/framework"
133
     * @return string
134
     */
135
    public function getPackageInfo($pkg)
136
    {
137
        $lockFileJSON = BASE_PATH . '/composer.lock';
138
139
        if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) {
140
            return self::SLW_NOOP;
141
        }
142
143
        $lockFileData = json_decode(file_get_contents($lockFileJSON), true);
144
145
        foreach ($lockFileData['packages'] as $package) {
146
            if ($package['name'] === $pkg) {
147
                return $package['version'];
148
            }
149
        }
150
        
151
        return self::SLW_NOOP;
152
    }
153
    
154
    /**
155
     * What sort of request is this? (A harder question to answer than you might
156
     * think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request)
157
     * 
158
     * @return string
159
     */
160
    public function getRequestType()
161
    {
162
        $isCLI = $this->getSAPI() !== 'cli';
163
        $isAjax = Director::is_ajax();
164
165
        return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax';
166
    }
167
    
168
    /**
169
     * Return peak memory usage.
170
     * 
171
     * @return float
172
     */
173
    public function getPeakMemory()
174
    {
175
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
176
        
177
        return (string) round($peak, 2) . 'Mb';
178
    }
179
    
180
    /**
181
     * Basic User-Agent check and return.
182
     * 
183
     * @return string
184
     */
185
    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...
186
    {
187
        $ua = @$_SERVER['HTTP_USER_AGENT'];
188
        
189
        if (!empty($ua)) {
190
            return $ua;
191
        }
192
        
193
        return self::SLW_NOOP;
194
    }
195
    
196
    /**
197
     * Basic request method check and return.
198
     * 
199
     * @return string
200
     */
201
    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...
202
    {
203
        $method = @$_SERVER['REQUEST_METHOD'];
204
        
205
        if (!empty($method)) {
206
            return $method;
207
        }
208
        
209
        return self::SLW_NOOP;
210
    }
211
    
212
    /**
213
     * @return string
214
     */
215
    public function getSAPI()
216
    {
217
        return php_sapi_name();
218
    }
219
    
220
 	/**
0 ignored issues
show
Coding Style introduced by
There is some trailing whitespace on this line which should be avoided as per coding-style.
Loading history...
221
	 * Returns the client IP address which originated this request.
222
     * Lifted and modified from SilverStripe 3's SS_HTTPRequest.
223
	 *
224
	 * @return string
225
	 */
226
	public function getIP()
0 ignored issues
show
Coding Style introduced by
getIP 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...
227
    {
228
		$headerOverrideIP = null;
229
        
230
		if(defined('TRUSTED_PROXY')) {
231
			$headers = (defined('SS_TRUSTED_PROXY_IP_HEADER')) ? 
232
                array(SS_TRUSTED_PROXY_IP_HEADER) : 
233
                null;
234
            
235
			if(!$headers) {
236
				// Backwards compatible defaults
237
				$headers = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR'];
238
			}
239
            
240
			foreach($headers as $header) {
241
				if(!empty($_SERVER[$header])) {
242
					$headerOverrideIP = $_SERVER[$header];
243
                    
244
					break;
245
				}
246
			}
247
		}
248
        
249
        $proxy = Injector::inst()->create('SilverStripe\Control\Middleware\TrustedProxyMiddleware');
250
251
		if ($headerOverrideIP) {
252
			return $proxy->getIPFromHeaderValue($headerOverrideIP);
253
		}
254
        
255
        if (isset($_SERVER['REMOTE_ADDR'])) {
256
			return $_SERVER['REMOTE_ADDR'];
257
		}
258
        
259
        return '';
260
	}
261
    
262
}
263