Skip to content
Browse files

Yay, error reporting

  • Loading branch information...
0 parents commit b402855ca82971997b4a7ab7b21c9da257d554f6 @d11wtq d11wtq committed Sep 11, 2012
Showing with 413 additions and 0 deletions.
  1. +20 −0 LICENSE
  2. +63 −0 README.md
  3. +110 −0 lib/Errbit.php
  4. +4 −0 lib/Errbit/Exception.php
  5. +123 −0 lib/Errbit/Notice.php
  6. +50 −0 lib/Errbit/XmlBuilder.php
  7. +43 −0 test.php
20 LICENSE
@@ -0,0 +1,20 @@
+Copyright © 2012 Flippa.com Pty. Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
63 README.md
@@ -0,0 +1,63 @@
+# Errbit & Airbrake Client for PHP
+
+This is a full-featured client to add integration with Errbit (or Airbrake)
+to any PHP >= 5.3 application.
+
+We had a number of issues with the
+[php-airbrake-notifier](https://github.com/geoloqi/php-airbrake-notifier)
+client, so we wrote this, based on the actual airbrake gem.
+
+The php-airbrake-notifier client would regularly try to send invalid XML
+to the Airbrake service and did not work at all with Errbit (the free,
+self-hosted Airbrake-compatible application).
+
+## Installation
+
+We haven't put this in PEAR or anything like that (please feel to contribute)
+so you need to install it locally.
+
+ git clone git://github.com/flippa/errbit-php
+
+## Usage
+
+The intended way to use the notifier is as a singleton, though this is not
+enforced and you may instantiate multiple instances if for some bizarre
+reason you need to, or the word singleton makes you cry unicorn tears.
+
+``` php
+require_once 'errbit-php/lib/Errbit.php';
+
+Errbit::instance()
+ ->configure(array(
+ 'api_key' => 'YOUR API KEY',
+ 'host' => 'YOUR ERRBIT HOST, OR api.airbrake.io FOR AIRBRAKE',
+ 'port' => 80, // optional
+ 'secure' => false, // optional
+ 'project_root' => '/your/project/root', // optional
+ 'environment_name' => 'production', // optional
+ 'params_filters' => array('/password/', '/card_number/'), // optional
+ 'backtrace_filters' => array('#/some/long/path#' => '') // optional
+ ))
+ ->start();
+```
+
+This will install error handlers that trap your PHP errors (according to
+your `error_reporting` settings) and log them to Errbit.
+
+If you want to notify an exception manually, you can call `notify()`.
+
+``` php
+try {
+ somethingErrorProne();
+} catch (Exception $e) {
+ Errbit::instance()->notify(
+ $e,
+ array('controller'=>'UsersController', 'action'=>'show')
+ );
+}
+```
+
+## License & Copyright
+
+Copyright © Flippa.com Pty. Ltd. Licensed under the MIT license. See the LICENSE
+file for details.
110 lib/Errbit.php
@@ -0,0 +1,110 @@
+<?php
+
+require_once dirname(__FILE__) . '/Errbit/Exception.php';
+require_once dirname(__FILE__) . '/Errbit/Notice.php';
+require_once dirname(__FILE__) . '/Errbit/XmlBuilder.php';
+
+class Errbit {
+ private static $_instance = null;
+
+ public static function instance() {
+ if (!isset(self::$_instance)) {
+ self::$_instance = new self();
+ }
+ return self::$_instance;
+ }
+
+ const VERSION = '0.0.1';
+ const API_VERSION = '2.2';
+ const PROJECT_NAME = 'errbit-php';
+ const PROJECT_URL = 'https://github.com/flippa/errbit-php';
+ const NOTICES_PATH = '/notifier_api/v2/notices/';
+
+ private $_config;
+
+ public function __construct($config = array()) {
+ $this->_config = $config;
+ }
+
+ public function configure($config = array()) {
+ $this->_config = $config;
+ $this->_checkConfig();
+ return $this;
+ }
+
+ public function start() {
+ $this->_checkConfig();
+ //Errbit_ErrorHandlers::register($this);
+ return $this;
+ }
+
+ public function notify(Exception $exception, $options = array()) {
+ //var_dump($this->_buildNoticeFor($exception, $options)); return;
+ $config = array_merge($this->_config, $options);
+
+ $ch = curl_init();
+ curl_setopt_array($ch, array(
+ CURLOPT_URL => $this->_buildApiUrl(),
+ CURLOPT_HEADER => true,
+ CURLOPT_POST => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_POSTFIELDS => $this->_buildNoticeFor($exception, $config),
+ CURLOPT_HTTPHEADER => array(
+ 'Content-Type: text/xml',
+ 'Accept: text/xml, application/xml'
+ )
+ ));
+ $response = curl_exec($ch);
+ var_dump($response);
+ return $this;
+ }
+
+ // -- Private Methods
+
+ private function _checkConfig() {
+ if (empty($this->_config['api_key'])) {
+ throw new Errbit_Exception("`api_key' must be configured");
+ }
+
+ if (empty($this->_config['host'])) {
+ throw new Errbit_Exception("`host' must be configured");
+ }
+
+ if (empty($this->_config['port'])) {
+ $this->_config['port'] = !empty($this->_config['secure']) ? 443 : 80;
+ }
+
+ if (!isset($this->_config['secure'])) {
+ $this->_config['secure'] = ($this->_config['port'] == 443);
+ }
+
+ if (empty($this->_config['hostname'])) {
+ $this->_config['hostname'] = gethostname() ? gethostname() : '<unknown>';
+ }
+
+ if (empty($this->_config['project_root'])) {
+ $this->_config['project_root'] = dirname(__FILE__);
+ }
+
+ if (empty($this->_config['environment_name'])) {
+ $this->_config['environment_name'] = 'development';
+ }
+ }
+
+ private function _buildApiUrl() {
+ $this->_checkConfig();
+ return implode(
+ '',
+ array(
+ $this->_config['secure'] ? 'https://' : 'http://',
+ $this->_config['host'],
+ ':' . $this->_config['port'],
+ self::NOTICES_PATH
+ )
+ );
+ }
+
+ private function _buildNoticeFor(Exception $exception, $options) {
+ return Errbit_Notice::forException($exception, $options)->asXml();
+ }
+}
4 lib/Errbit/Exception.php
@@ -0,0 +1,4 @@
+<?php
+
+class Errbit_Exception extends Exception {
+}
123 lib/Errbit/Notice.php
@@ -0,0 +1,123 @@
+<?php
+
+class Errbit_Notice {
+ private $_exception;
+ private $_options;
+
+ public function __construct(Exception $exception, $options = array()) {
+ $this->_exception = $exception;
+ $this->_options = array_merge(
+ array(
+ 'url' => !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null,
+ 'parameters' => !empty($_REQUEST) ? $_REQUEST : array(),
+ 'session' => !empty($_SESSION) ? $_SESSION : array(),
+ 'cgi_data' => !empty($_SERVER) ? $_SERVER : array()
+ ),
+ $options
+ );
+
+ $this->_filterData();
+ }
+
+ public static function forException(Exception $exception, $options = array()) {
+ return new self($exception, $options);
+ }
+
+ public static function formatMethod($frame) {
+ if (!empty($frame['class']) && !empty($frame['type']) && !empty($frame['function'])) {
+ return sprintf('%s%s%s()', $frame['class'], $frame['type'], $frame['function']);
+ } else {
+ return sprintf('%s()', !empty($frame['function']) ? $frame['function'] : '<unknown>');
+ }
+ }
+
+ public static function xmlVarsFor($builder, $array) {
+ foreach ($array as $key => $value) {
+ if (is_array($value)) {
+ $builder->tag('var', array('key' => $key), function($var) use ($value) {
+ Errbit_Notice::xmlVarsFor($var, $value);
+ });
+ } else {
+ $builder->tag('var', $value, array('key' => $key));
+ }
+ }
+ }
+
+ public function asXml() {
+ $exception = $this->_exception;
+ $options = $this->_options;
+ $builder = new Errbit_XmlBuilder();
+
+ return $builder->tag(
+ 'notice',
+ array('version' => Errbit::API_VERSION),
+ function($notice) use ($exception, $options) {
+ $notice->tag('api-key', $options['api_key']);
+ $notice->tag('notifier', function($notifier) {
+ $notifier->tag('name', Errbit::PROJECT_NAME);
+ $notifier->tag('version', Errbit::VERSION);
+ $notifier->tag('url', Errbit::PROJECT_URL);
+ });
+
+ $notice->tag('error', function($error) use ($exception) {
+ $error->tag('class', get_class($exception));
+ $error->tag('message', $exception->getMessage());
+ $error->tag('backtrace', function($backtrace) use ($exception) {
+ foreach ($exception->getTrace() as $frame) {
+ $backtrace->tag(
+ 'line',
+ array(
+ 'number' => isset($frame['line']) ? $frame['line'] : 0,
+ 'file' => isset($frame['file']) ? $frame['file'] : '<unknown>',
+ 'method' => Errbit_Notice::formatMethod($frame)
+ )
+ );
+ }
+ });
+ });
+
+ if (!empty($options['url'])
+ || !empty($options['controller'])
+ || !empty($options['action'])
+ || !empty($options['parameters'])
+ || !empty($options['session_data'])
+ || !empty($options['cgi_data'])) {
+ $notice->tag('request', function($request) use ($options) {
+ $request->tag('url', !empty($options['url']) ? $options['url'] : '');
+ $request->tag('component', !empty($options['controller']) ? $options['controller'] : '');
+ $request->tag('action', !empty($options['action']) ? $options['action'] : '');
+ if (!empty($options['parameters'])) {
+ $request->tag('params', function($params) use ($options) {
+ Errbit_Notice::xmlVarsFor($params, $options['parameters']);
+ });
+ }
+
+ if (!empty($options['session_data'])) {
+ $request->tag('session', function($session) use ($options) {
+ Errbit_Notice::xmlVarsFor($session, $options['session_data']);
+ });
+ }
+
+ if (!empty($options['cgi_data'])) {
+ $request->tag('cgi-data', function($cgiData) use ($options) {
+ Errbit_Notice::xmlVarsFor($cgiData, $options['cgi_data']);
+ });
+ }
+ });
+ }
+
+ $notice->tag('server-environment', function($env) use ($options) {
+ $env->tag('project-root', $options['project_root']);
+ $env->tag('environment-name', $options['environment_name']);
+ $env->tag('hostname', $options['hostname']);
+ });
+ }
+ )->asXml();
+ }
+
+ // -- Private Methods
+
+ private function _filterData() {
+ // FIXME: consult $this->_options['blacklist'] ? and filter the stuff
+ }
+}
50 lib/Errbit/XmlBuilder.php
@@ -0,0 +1,50 @@
+<?php
+
+class Errbit_XmlBuilder {
+ public function __construct($xml = null) {
+ $this->_xml = $xml ? $xml : new SimpleXMLElement('<__ErrbitXMLBuilder__/>');
+ }
+
+ public function tag($name /* , $value, $attributes, $callback */) {
+ $value = '';
+ $attributes = array();
+ $callback = null;
+ $idx = count($this->_xml->$name);
+ $args = func_get_args();
+
+ array_shift($args);
+ foreach ($args as $arg) {
+ if (is_string($arg)) {
+ $value = $arg;
+ } elseif (is_callable($arg)) {
+ $callback = $arg;
+ } elseif (is_array($arg)) {
+ $attributes = $arg;
+ }
+ }
+
+ $this->_xml->{$name}[$idx] = $value;
+
+ foreach ($attributes as $attr => $v) {
+ $this->_xml->{$name}[$idx][$attr] = $v;
+ }
+
+ // FIXME: This isn't the last child, it's the first, it just doesn't happen to matter in this project
+ $node = new self($this->_xml->$name);
+
+ if ($callback) {
+ $callback($node);
+ }
+
+ return $node;
+ }
+
+ public function attribute($name, $value) {
+ $this->_xml[$name] = $value;
+ return $this;
+ }
+
+ public function asXml() {
+ return $this->_xml->asXML();
+ }
+}
43 test.php
@@ -0,0 +1,43 @@
+<?php
+
+require_once 'lib/Errbit.php';
+
+$client = Errbit::instance();
+$client->configure(array(
+ 'api_key' => 'fedfb7520ef06c3686cc35fe338b6c58',
+ 'host' => 'flippa-errbit.herokuapp.com',
+ 'port' => 443,
+ 'secure' => true
+));
+
+class BobException extends Exception {
+}
+
+class Bob {
+ public static function doIt() {
+ $bob = new self();
+ $bob->a();
+ }
+
+ public function a() {
+ $this->b();
+ }
+
+ public function b() {
+ $this->c();
+ }
+
+ public function c() {
+ throw new BobException('Example error message');
+ }
+}
+
+function callBob() {
+ Bob::doIt();
+}
+
+try {
+ callBob();
+} catch (Exception $e) {
+ $client->notify($e, array('url' => 'http://bob.com/thang', 'controller'=>'BobController', 'action'=>'showBob'));
+}

0 comments on commit b402855

Please sign in to comment.
Something went wrong with that request. Please try again.