PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/src/Airbrake/Notice.class.php

https://github.com/Work4Labs/php-airbrake
PHP | 344 lines | 221 code | 34 blank | 89 comment | 43 complexity | fb2796de71a6117b36914d928d7f8606 MD5 | raw file
  1. <?php
  2. namespace Airbrake;
  3. /**
  4. * Airbrake notice class.
  5. *
  6. * @package Airbrake
  7. * @author Drew Butler <drew@abstracting.me>
  8. * @copyright (c) 2011 Drew Butler
  9. * @license http://www.opensource.org/licenses/mit-license.php
  10. */
  11. class Notice extends Record
  12. {
  13. /**
  14. * The backtrace from the given exception or hash.
  15. */
  16. protected $_backtrace = null;
  17. /**
  18. * The message from the exception, or a general description of the error
  19. */
  20. protected $_errorMessage = null;
  21. /**
  22. * The event level
  23. */
  24. protected $_level = null;
  25. /**
  26. * The Sentry event ID
  27. */
  28. protected $_eventId = null;
  29. /**
  30. * Additional 'extra' data for the current notice
  31. */
  32. protected $_additionalExtra = null;
  33. /**
  34. * Additional 'tags' data for the current notice
  35. */
  36. protected $_additionalTags = null;
  37. /**
  38. * The memoized JSON
  39. */
  40. protected $_json = null;
  41. /**
  42. * The current Sentry configuration
  43. */
  44. protected $_configuration = null;
  45. // the max length for a string representing an array argument of a function
  46. const MAX_ARRAY_ARG_STRING_LENGTH = 1000;
  47. // the max length for a string representing a single argument of a function
  48. const MAX_SINGLE_ARG_STRING_LENGTH = 200;
  49. public function __construct(Configuration $configuration, $data = array())
  50. {
  51. $this->configuration = $configuration;
  52. parent::__construct($data);
  53. }
  54. /**
  55. * Returns the JSON expected by Sentry
  56. *
  57. * @return string
  58. */
  59. public function getJSON()
  60. {
  61. $this->buildJSON();
  62. return $this->_json;
  63. }
  64. /**
  65. * Returns the Sentry Event ID
  66. * (generates the JSON if not done yet)
  67. *
  68. * @return string
  69. */
  70. public function getEventId()
  71. {
  72. $this->buildJSON();
  73. return $this->eventId;
  74. }
  75. /**
  76. * Converts the notice to JSON, and also saves it in the DB if applicable
  77. */
  78. private function buildJSON()
  79. {
  80. if ($this->_json !== null) {
  81. return;
  82. }
  83. $configuration = $this->configuration;
  84. $timestamp = time();
  85. // basic options
  86. $result = array(
  87. 'message' => static::sanitizeMessage($this->errorMessage, $configuration),
  88. 'timestamp' => date('c', $timestamp),
  89. 'level' => $this->level,
  90. 'logger' => Version::NAME,
  91. 'platform' => $configuration->platform,
  92. 'servername' => gethostname()
  93. );
  94. // tags and extras
  95. foreach (array('tags', 'extra') as $key) {
  96. // first from the config
  97. if (($callback = $configuration->get($key.'Callback')) && ($data = $callback->call())) {
  98. $result[$key] = $data;
  99. }
  100. // then local ones for this notice
  101. $attribute = 'additional'.ucfirst($key);
  102. if ($this->$attribute) {
  103. if (array_key_exists($key, $result)) {
  104. $result[$key] = array_merge($result[$key], $this->$attribute);
  105. } else {
  106. $result[$key] = $this->$attribute;
  107. }
  108. }
  109. }
  110. // now to the backtrace - do we need to compute backtrace arguments?
  111. $computeArgs = $configuration->sendArgumentsToAirbrake || (bool) $configuration->arrayReportDatabaseClass;
  112. if ($this->backtrace) {
  113. $frames = array();
  114. // we need to reverse the backtrace as Sentry expects the most recent event first
  115. for ($i = count($this->backtrace) - 1; $i >= 0; $i--) {
  116. if ($frame = self::getStacktraceFrame($this->backtrace[$i], $configuration, $computeArgs)) {
  117. $frames[] = $frame;
  118. }
  119. }
  120. if ($frames) {
  121. $result['sentry.interfaces.Stacktrace']['frames'] = $frames;
  122. }
  123. }
  124. // and finally other interfaces!
  125. if (($interfacesCallback = $configuration->interfacesCallback) && ($interfaces = $interfacesCallback->call())) {
  126. foreach ($interfaces as $name => $data) {
  127. if ($data) {
  128. $result['sentry.interfaces.'.$name] = $data;
  129. }
  130. }
  131. }
  132. // last but not least, the event id
  133. if ($arrayReportDatabaseClass = $configuration->arrayReportDatabaseClass) {
  134. // then we should be able to get the ID from the DB!
  135. try {
  136. if (!($dbId = $arrayReportDatabaseClass::logInDB($result, $timestamp))) {
  137. throw new \Exception('Error while logging Airbrake report into DB');
  138. }
  139. $eventId = self::formatUuid($dbId);
  140. } catch (\Exception $ex) {
  141. $eventId = null;
  142. $configuration->notifyUpperLayer($ex);
  143. }
  144. }
  145. if (empty($eventId)) {
  146. $eventId = self::getRandomUuid4();
  147. }
  148. $result['event_id'] = $eventId;
  149. // we also add it to the actual report to be able to cross-reference to the DB
  150. $result['extra']['event_id'] = $eventId;
  151. $this->eventId = $eventId;
  152. if (!empty($dbId)) {
  153. $result['extra']['db_id'] = $dbId;
  154. }
  155. if ($computeArgs && !$configuration->sendArgumentsToAirbrake) {
  156. // we need to remove the arguments from the backtrace before sending them to Airbrake
  157. self::pruneArgs($result);
  158. }
  159. $this->_json = json_encode($result);
  160. }
  161. // removes any unwanted string from the error message
  162. private static function sanitizeMessage($errorMessage, Configuration $configuration)
  163. {
  164. $blacklistedStrings = $configuration->blacklistedStringsInMsgCallback ? $configuration->blacklistedStringsInMsgCallback->call() : null;
  165. if (!$blacklistedStrings) {
  166. return $errorMessage;
  167. }
  168. return str_replace($blacklistedStrings,
  169. array_fill(0, count($blacklistedStrings), '[BLACKLISTED STRING]'),
  170. $errorMessage);
  171. }
  172. // generates a stacktrace for this entry (see http://sentry.readthedocs.org/en/latest/developer/interfaces/index.html)
  173. private static function getStacktraceFrame(array $entry, Configuration $configuration, $computeArgs = true)
  174. {
  175. if (!isset($entry['function']) && !isset($entry['file'])) {
  176. return null;
  177. }
  178. $result = array();
  179. // the function name
  180. if (isset($entry['function'])) {
  181. $function = $entry['function'];
  182. // prepend the class name and function type if available
  183. if (isset($entry['class']) && isset($entry['type'])) {
  184. $function = $entry['class'].$entry['type'].$function;
  185. }
  186. $result['function'] = $function;
  187. }
  188. // file and line number
  189. if (isset($entry['file'])) {
  190. $result['filename'] = $entry['file'];
  191. }
  192. if (isset($entry['line'])) {
  193. $result['lineno'] = $entry['line'];
  194. }
  195. // compute arguments, if necessary
  196. if ($computeArgs) {
  197. $args = array();
  198. if (isset($entry['args'])) {
  199. $i = 1;
  200. foreach ($entry['args'] as $arg) {
  201. // numbering args in that way is kinda ugly, but necessary to comply with Sentry's expected format
  202. $args[(string) $i++] = self::argToString($arg, $configuration);
  203. }
  204. }
  205. $result['vars'] = $args;
  206. }
  207. return $result;
  208. }
  209. // returns a string to represent any argument
  210. // more specifically, objects are just represented by their class' name
  211. // resources by their type
  212. // and all others are var_export'ed
  213. const MAX_LEVEL = 10; // the maximum level up to which arrays will be exported (inclusive)
  214. private static function argToString($arg, Configuration $configuration, $level = 1)
  215. {
  216. if ($arg === null) {
  217. return 'NULL';
  218. }
  219. $maxLength = self::MAX_SINGLE_ARG_STRING_LENGTH;
  220. if (is_array($arg) || $arg instanceof Traversable) {
  221. $result = self::singleArgToString($arg, $configuration).' (';
  222. if ($level > self::MAX_LEVEL) {
  223. $result .= '... TOO MANY LEVELS IN THE ARRAY, NOT DISPLAYED ...';
  224. } else {
  225. $arrayString = '';
  226. foreach ($arg as $key => $value) {
  227. $arrayString .= ($arrayString ? ', ' : '').var_export($key, true).' => '.self::argToString($value, $configuration, $level + 1);
  228. }
  229. $result .= $arrayString;
  230. }
  231. $result .= ')';
  232. $maxLength = self::MAX_ARRAY_ARG_STRING_LENGTH;
  233. } else {
  234. $result = self::singleArgToString($arg, $configuration);
  235. }
  236. if (strlen($result) > $maxLength) {
  237. $result = substr($result, 0, self::MAX_SINGLE_ARG_STRING_LENGTH);
  238. $result .= ' ... [ARG TRUNCATED]';
  239. }
  240. return $result;
  241. }
  242. private static function singleArgToString($arg, Configuration $configuration)
  243. {
  244. if (is_object($arg)) {
  245. $stringifierCallback = $configuration->objectStringifierCallback;
  246. $appendedString = $stringifierCallback ? $stringifierCallback->call($arg) : '';
  247. return 'Object '.get_class($arg).($appendedString ? ' : '.$appendedString : '');
  248. } elseif (is_resource($arg)) {
  249. return 'Resource '.get_resource_type($arg);
  250. } elseif (is_array($arg)) {
  251. return 'array';
  252. // should be a scalar then, let's see if it's blacklisted
  253. } elseif($configuration->isScalarBlackListed($arg)) {
  254. return '[BLACKLISTED SCALAR]';
  255. } else {
  256. // a scalar, not blacklisted!
  257. return gettype($arg).' '.var_export($arg, true);
  258. }
  259. }
  260. // from http://sentry.readthedocs.org/en/latest/developer/client/ : the uuid is a 32-char long hex string
  261. const UUID_LENGTH = 32;
  262. // checks the uuid is not too long, is an hex string, and pads it if necessary with leading zeroes
  263. public static function formatUuid($uuid) {
  264. $uuid = (string) $uuid;
  265. if (strlen($uuid) > self::UUID_LENGTH) {
  266. throw new \Exception('UUID "'.$uuid.'" is too long! Can\'t be more than '.self::UUID_LENGTH.' characters long');
  267. }
  268. if (!preg_match('/^[a-fA-f0-9]*$/', $uuid)) {
  269. throw new \Exception('UUID must be an hexadecimal string! You gave '.$uuid);
  270. }
  271. $padding = str_repeat('0', self::UUID_LENGTH - strlen($uuid));
  272. return $padding.$uuid;
  273. }
  274. // deletes arguments from the backtrace
  275. // useful when we want to save them to a local DB but not send them to Airbrake
  276. private static function pruneArgs(array &$report)
  277. {
  278. if (array_key_exists('sentry.interfaces.Stacktrace', $report)) {
  279. foreach ($report['sentry.interfaces.Stacktrace']['frames'] as &$frame) {
  280. if (array_key_exists('vars', $frame)) {
  281. unset($frame['vars']);
  282. }
  283. }
  284. }
  285. }
  286. /**
  287. * Generates a random uuid4 value
  288. * Only called if no local DB class is provided
  289. * Official spec, implementation taken from the official php-raven source code
  290. */
  291. private static function getRandomUuid4()
  292. {
  293. $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
  294. // 32 bits for "time_low"
  295. mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
  296. // 16 bits for "time_mid"
  297. mt_rand( 0, 0xffff ),
  298. // 16 bits for "time_hi_and_version",
  299. // four most significant bits holds version number 4
  300. mt_rand( 0, 0x0fff ) | 0x4000,
  301. // 16 bits, 8 bits for "clk_seq_hi_res",
  302. // 8 bits for "clk_seq_low",
  303. // two most significant bits holds zero and one for variant DCE1.1
  304. mt_rand( 0, 0x3fff ) | 0x8000,
  305. // 48 bits for "node"
  306. mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
  307. );
  308. return str_replace('-', '', $uuid);
  309. }
  310. }