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; |
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(\phptek\Sentry\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
|
|
|
* A static constructor as per {@link Zend_Log_FactoryInterface}. |
36
|
|
|
* |
37
|
|
|
* @param array $config An array of optional additional configuration for |
38
|
|
|
* passing custom information to Sentry. See the README for more detail. |
39
|
|
|
* @return SentryLogWriter |
40
|
|
|
*/ |
41
|
|
|
public static function factory($config = []) |
42
|
|
|
{ |
43
|
|
|
$env = isset($config['env']) ? $config['env'] : null; |
44
|
|
|
$tags = isset($config['tags']) ? $config['tags'] : []; |
45
|
|
|
$extra = isset($config['extra']) ? $config['extra'] : []; |
46
|
|
|
$writer = \Injector::inst()->get('SentryLogWriter'); |
47
|
|
|
|
48
|
|
|
// Set default environment |
49
|
|
|
if (is_null($env)) { |
50
|
|
|
$env = $writer->defaultEnv(); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
// Set all available user-data |
54
|
|
|
$userData = $writer->defaultUserData(); |
55
|
|
|
|
56
|
|
|
if ($member = \Member::currentUser()) { |
57
|
|
|
$userData = $writer->defaultUserData($member); |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
// Set any available tags available in SS config |
61
|
|
|
$tags = array_merge($writer->defaultTags(), $tags); |
62
|
|
|
|
63
|
|
|
// Set any avalable additional (extra) data |
64
|
|
|
$extra = array_merge($writer->defaultExtra(), $extra); |
65
|
|
|
|
66
|
|
|
$writer->client->setData('env', $env); |
67
|
|
|
$writer->client->setData('user', $userData); |
68
|
|
|
$writer->client->setData('tags', $tags); |
69
|
|
|
$writer->client->setData('extra', $extra); |
70
|
|
|
|
71
|
|
|
return $writer; |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Used in unit tests. |
76
|
|
|
* |
77
|
|
|
* @return SentryClientAdaptor |
78
|
|
|
*/ |
79
|
|
|
public function getClient() |
80
|
|
|
{ |
81
|
|
|
return $this->client; |
|
|
|
|
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* Returns a default environment when one isn't passed to the factory() |
86
|
|
|
* method. |
87
|
|
|
* |
88
|
|
|
* @return string |
89
|
|
|
*/ |
90
|
|
|
public function defaultEnv() |
91
|
|
|
{ |
92
|
|
|
return \Director::get_environment_type(); |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Returns a default set of additional data specific to the user's part in |
97
|
|
|
* the request. |
98
|
|
|
* |
99
|
|
|
* @param Member $member |
100
|
|
|
* @return array |
101
|
|
|
*/ |
102
|
|
|
public function defaultUserData(\Member $member = null) |
103
|
|
|
{ |
104
|
|
|
return [ |
105
|
|
|
'IP-Address' => $this->getIP(), |
106
|
|
|
'ID' => $member ? $member->getField('ID') : self::SLW_NOOP, |
107
|
|
|
'Email' => $member ? $member->getField('Email') : self::SLW_NOOP, |
108
|
|
|
]; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Returns a default set of additional "tags" we wish to send to Sentry. |
113
|
|
|
* By default, Sentry reports on several mertrics, and we're already sending |
114
|
|
|
* {@link Member} data. But there are additional data that would be useful |
115
|
|
|
* for debugging via the Sentry UI. |
116
|
|
|
* |
117
|
|
|
* These data can augment that which is sent to Sentry at setup |
118
|
|
|
* time in _config.php. See the README for more detail. |
119
|
|
|
* |
120
|
|
|
* N.b. Tags can be used to group messages within the Sentry UI itself, so there |
121
|
|
|
* should only be "static" data being sent, not something that can drastically |
122
|
|
|
* or minutely change, such as memory usage for example. |
123
|
|
|
* |
124
|
|
|
* @return array |
125
|
|
|
*/ |
126
|
|
|
public function defaultTags() |
127
|
|
|
{ |
128
|
|
|
return [ |
129
|
|
|
'Request-Method'=> $this->getReqMethod(), |
130
|
|
|
'Request-Type' => $this->getRequestType(), |
131
|
|
|
'SAPI' => $this->getSAPI(), |
132
|
|
|
'SS-Version' => $this->getPackageInfo('silverstripe/framework') |
133
|
|
|
]; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Returns a default set of extra data to show upon selecting a message for |
138
|
|
|
* analysis in the Sentry UI. This can augment the data sent to Sentry at setup |
139
|
|
|
* time in _config.php as well as at runtime when calling SS_Log itself. |
140
|
|
|
* See the README for more detail. |
141
|
|
|
* |
142
|
|
|
* @return array |
143
|
|
|
*/ |
144
|
|
|
public function defaultExtra() |
145
|
|
|
{ |
146
|
|
|
return [ |
147
|
|
|
'Peak-Memory' => $this->getPeakMemory() |
148
|
|
|
]; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* _write() forms the entry point into the physical sending of the error. The |
153
|
|
|
* sending itself is done by the current adaptor's `send()` method. |
154
|
|
|
* |
155
|
|
|
* @param array $event An array of data that is created in, and arrives here |
156
|
|
|
* via {@link SS_Log::log()} and {@link Zend_Log::log}. |
157
|
|
|
* @return void |
158
|
|
|
*/ |
159
|
|
|
protected function _write($event) |
160
|
|
|
{ |
161
|
|
|
$message = $event['message']['errstr']; // From SS_Log::log() |
|
|
|
|
162
|
|
|
// The complete compliment of these data come via the Raven_Client::xxx_context() methods |
163
|
|
|
$data = [ |
164
|
|
|
'timestamp' => strtotime($event['timestamp']), // From Zend_Log::log() |
|
|
|
|
165
|
|
|
'extra' => isset($event['extra']) ? $event['extra'] : '' // From _config.php (Optional) |
166
|
|
|
]; |
167
|
|
|
$trace = \SS_Backtrace::filter_backtrace(debug_backtrace(), ['SentryLogWriter->_write']); |
168
|
|
|
|
169
|
|
|
$this->client->send($message, [], $data, $trace); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Return the version of $pkg taken from composer.lock. |
174
|
|
|
* |
175
|
|
|
* @param string $pkg e.g. "silverstripe/framework" |
176
|
|
|
* @return string |
177
|
|
|
*/ |
178
|
|
|
public function getPackageInfo($pkg) |
179
|
|
|
{ |
180
|
|
|
$lockFileJSON = BASE_PATH . '/composer.lock'; |
181
|
|
|
|
182
|
|
|
if (!file_exists($lockFileJSON) || !is_readable($lockFileJSON)) { |
183
|
|
|
return self::SLW_NOOP; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
$lockFileData = json_decode(file_get_contents($lockFileJSON), true); |
187
|
|
|
|
188
|
|
|
foreach ($lockFileData['packages'] as $package) { |
189
|
|
|
if ($package['name'] === $pkg) { |
190
|
|
|
return $package['version']; |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
return self::SLW_NOOP; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Return the IP address of the relevant request. |
199
|
|
|
* |
200
|
|
|
* @return string |
201
|
|
|
*/ |
202
|
|
|
public function getIP() |
203
|
|
|
{ |
204
|
|
|
$req = \Injector::inst()->create('SS_HTTPRequest', $this->getReqMethod(), ''); |
205
|
|
|
|
206
|
|
|
if ($ip = $req->getIP()) { |
207
|
|
|
return $ip; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
return self::SLW_NOOP; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* What sort of request is this? (A harder question to answer than you might |
215
|
|
|
* think: http://stackoverflow.com/questions/6275363/what-is-the-correct-terminology-for-a-non-ajax-request) |
216
|
|
|
* |
217
|
|
|
* @return string |
218
|
|
|
*/ |
219
|
|
|
public function getRequestType() |
220
|
|
|
{ |
221
|
|
|
$isCLI = $this->getSAPI() !== 'cli'; |
222
|
|
|
$isAjax = \Director::is_ajax(); |
223
|
|
|
|
224
|
|
|
return $isCLI && $isAjax ? 'AJAX' : 'Non-Ajax'; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Return peak memory usage. |
229
|
|
|
* |
230
|
|
|
* @return float |
231
|
|
|
*/ |
232
|
|
|
public function getPeakMemory() |
233
|
|
|
{ |
234
|
|
|
$peak = memory_get_peak_usage(true) / 1024 / 1024; |
235
|
|
|
|
236
|
|
|
return (string) round($peak, 2) . 'Mb'; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
/** |
240
|
|
|
* Basic User-Agent check and return. |
241
|
|
|
* |
242
|
|
|
* @return string |
243
|
|
|
*/ |
244
|
|
|
public function getUserAgent() |
|
|
|
|
245
|
|
|
{ |
246
|
|
|
$ua = @$_SERVER['HTTP_USER_AGENT']; |
247
|
|
|
|
248
|
|
|
if (!empty($ua)) { |
249
|
|
|
return $ua; |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return self::SLW_NOOP; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Basic reuqest method check and return. |
257
|
|
|
* |
258
|
|
|
* @return string |
259
|
|
|
*/ |
260
|
|
|
public function getReqMethod() |
|
|
|
|
261
|
|
|
{ |
262
|
|
|
$method = @$_SERVER['REQUEST_METHOD']; |
263
|
|
|
|
264
|
|
|
if (!empty($method)) { |
265
|
|
|
return $method; |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
return self::SLW_NOOP; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* @return string |
273
|
|
|
*/ |
274
|
|
|
public function getSAPI() |
275
|
|
|
{ |
276
|
|
|
return php_sapi_name(); |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
} |
280
|
|
|
|
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.