├── screenshot-1.png ├── screenshot-2.png ├── go-newrelic.php ├── deploy └── push-to-wporg-repo.php ├── components ├── class-go-newrelic-apm.php ├── class-go-newrelic.php ├── class-go-newrelic-wpcli.php └── class-go-newrelic-browser.php └── README.md /screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GigaOM/go-newrelic/HEAD/screenshot-1.png -------------------------------------------------------------------------------- /screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GigaOM/go-newrelic/HEAD/screenshot-2.png -------------------------------------------------------------------------------- /go-newrelic.php: -------------------------------------------------------------------------------- 1 | $svn_repo_path/readme.txt" ); 59 | 60 | echo ' 61 | Removing any svn:executable properties for security 62 | '; 63 | passthru( "find $svn_repo_path -type f -not -iwholename *svn* -exec svn propdel svn:executable {} \; | grep 'deleted from'" ); 64 | 65 | echo ' 66 | Setting svn:ignore properties 67 | '; 68 | passthru( "svn propset svn:ignore '" . implode( "\n", $svn_ignore_files ) ." 69 | ' $svn_repo_path 70 | " ); 71 | 72 | passthru( "svn proplist -v $svn_repo_path" ); 73 | 74 | echo ' 75 | Marking deleted files for removal from the SVN repo 76 | '; 77 | passthru( "svn st $svn_repo_path | grep '^\!' | sed 's/\!\s*//g' | xargs svn rm" ); 78 | 79 | echo ' 80 | Marking new files for addition to the SVN repo 81 | '; 82 | passthru( "svn st $svn_repo_path | grep '^\?' | sed 's/\?\s*//g' | xargs svn add" ); 83 | 84 | echo ' 85 | Now forcibly removing the files that are supposed to be ignored in the svn repo 86 | '; 87 | foreach( $svn_ignore_files as $file ) 88 | { 89 | passthru( "svn rm --force $svn_repo_path/$file" ); 90 | } 91 | 92 | 93 | echo " 94 | Automatic processes complete! 95 | 96 | Next steps: 97 | 98 | `cd $svn_repo_path` and review the changes 99 | `svn commit` the changes 100 | profit 101 | 102 | * svn diff -x \"-bw --ignore-eol-style\" | grep \"^Index:\" | sed 's/^Index: //g' will be your friend if there are a lot of whitespace changes 103 | 104 | Good luck! 105 | "; 106 | -------------------------------------------------------------------------------- /components/class-go-newrelic-apm.php: -------------------------------------------------------------------------------- 1 | go_newrelic = $go_newrelic; 12 | 13 | // can't lazy load the config, we need 14 | $this->config = $this->go_newrelic->config(); 15 | 16 | // the license key is typically set elsewhere during the daemon/module installation, 17 | // but this allows some potential future where the license key is set in the WP dashboard 18 | if ( ! empty( $this->config['license'] ) ) 19 | { 20 | ini_set( 'newrelic.license', $this->config['license'] ); 21 | }// END if 22 | 23 | // basic settings 24 | // make sure the config isn't empty or invalid for any of these 25 | // ...sanity and validation intentionally skipped for performance reasons 26 | ini_set( 'newrelic.framework', 'wordpress' ); 27 | ini_set( 'newrelic.transaction_tracer.detail', $this->config['transaction-tracer-detail'] ); 28 | ini_set( 'newrelic.error_collector.enabled', $this->config['error-collector-enabled'] ); 29 | if ( isset( $this->config['capture-params'] ) && $this->config['capture-params'] ) 30 | { 31 | newrelic_capture_params(); 32 | }// END if 33 | ini_set( 'newrelic.ignored_params', $this->go_newrelic->config( 'ignored-params' ) ); 34 | 35 | // set logging parameters based on request context 36 | // ajax responses _cannot_ have RUM in them, for example 37 | if ( is_admin() ) 38 | { 39 | if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) 40 | { 41 | newrelic_disable_autorum(); 42 | }// END if 43 | }// end if 44 | elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) 45 | { 46 | newrelic_disable_autorum(); 47 | }// END elseif 48 | else 49 | { 50 | // add more tracking of the template pieces 51 | add_action( 'template_include', array( $this, 'template_include' ) ); 52 | }// END else 53 | 54 | // track the user info and more 55 | add_action( 'init', array( $this, 'init' ) ); 56 | }//end __construct 57 | 58 | /** 59 | * add more info now that we know it 60 | */ 61 | public function init() 62 | { 63 | // set the app name 64 | newrelic_set_appname( $this->go_newrelic->get_appname() ); 65 | 66 | // not all versions of the php extension support this method 67 | if ( ! function_exists( 'newrelic_set_user_attributes' ) ) 68 | { 69 | return; 70 | }// END if 71 | 72 | // see https://newrelic.com/docs/features/browser-traces#set_user_attributes 73 | // for docs on how to use the user info in the transaction trace 74 | if ( is_user_logged_in() ) 75 | { 76 | $user = wp_get_current_user(); 77 | newrelic_set_user_attributes( $user->ID, '', array_shift( $user->roles ) ); 78 | }// END if 79 | else 80 | { 81 | newrelic_set_user_attributes( 'not-logged-in', '', 'no-role' ); 82 | }// END else 83 | }// END init 84 | 85 | /** 86 | * track the template we're using 87 | */ 88 | public function template_include( $template ) 89 | { 90 | newrelic_add_custom_parameter( 'template', $template ); 91 | return $template; 92 | }// END template_include 93 | 94 | /** 95 | * a method other plugins can call to ignore this transaction 96 | */ 97 | public function ignore() 98 | { 99 | newrelic_ignore_transaction(); 100 | newrelic_ignore_apdex(); 101 | }// END ignore 102 | }// end class -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # === Gigaom New Relic === 2 | 3 | Contributors: misterbisson, zbtirrell 4 | 5 | Tags: monitoring, telemetry, server monitoring, performance monitoring, newrelic, new relic, Gigaom 6 | 7 | Requires at least: 3.5.1 8 | 9 | Tested up to: 4.0 10 | 11 | Stable tag: trunk 12 | 13 | Configures New Relic to better track performance, errors, and uptime of WordPress sites, including multisite 14 | 15 | ## == Description == 16 | 17 | Supports both New Relic APM and Browser monitoring to give a clear picture of how your site performs both on the server and in the browser. 18 | 19 | ### = Application Performance Monitoring (APM) = 20 | 21 | Automatically detects if the APM extensions are installed on the server. If so, the plugin will start reporting into the New Relic account associated with the license key used when installing the extension. 22 | 23 | There's no UI, but the plugin automatically sets the app name and other configuration values ideally for each request. The app name is based on the blog's name. User-facing and dashboard activity are reported as separate apps so you can set different QoS and alert settings for each. Even cron and admin-ajax activity are separated out for individual tracking. 24 | 25 | Each blog in a multi-site installation is tracked separately, using the name of the blog as the app name. 26 | 27 | ### = Browser monitoring (RUM) = 28 | 29 | Real user monitoring (browser monitoring) is automatically enabled if the APM extension is active, but in situations where the APM extension can't be used, the plugin can still be used to track browser performance. 30 | 31 | This mode requires some configuration: 32 | 33 | 1. Get the tracking JavaScript from New Relic. 34 | 1. Go to your WordPress dashboard -> Settings -> New Relic Settings and paste in the JavaScript 35 | 1. Go to the New Relic dashboard to see your site reporting performance data! 36 | 37 | The plugin extracts the configuration details from the JS and inserts them with a clean copy of the JS on each page (this cannot be used to inject arbitrary JS into the page). 38 | 39 | Due to limitations of the Browser monitoring service/API, Browser-only monitoring does not include all the data or separate reporting of activity in separate apps as APM does. 40 | 41 | ### = In the WordPress.org plugin repo = 42 | 43 | Here: https://wordpress.org/plugins/go-newrelic/ 44 | 45 | ### = Fork me! = 46 | 47 | This plugin is on Github: https://github.com/gigaOM/go-newrelic 48 | 49 | ## == Installation == 50 | 51 | 1. Install and activate New Relic's PHP agent, https://docs.newrelic.com/docs/php/new-relic-for-php#installation 52 | 1. The web server should appear in New Relic's dashboard 53 | 1. Download and activate this plugin from http://wordpress.org/plugins/go-newrelic/ 54 | 1. Go back to the New Relic dashboard and enjoy monitoring each WordPress blog (and different aspects of each blog) 55 | 1. Follow the Gigaom engineering team at http://kitchen.gigaom.com and https://github.com/gigaom/ 56 | 57 | ## == Screenshots == 58 | 59 | 1. New Relic application list, showing two blogs. Each WordPress blog is reported as four applications in New Relic to separate reader, writer, cron, and admin-ajax activity for better detail and fine-grained control. 60 | 2. New Relic application overview, showing performance history for a single app. 61 | -------------------------------------------------------------------------------- /components/class-go-newrelic.php: -------------------------------------------------------------------------------- 1 | apm(); 17 | }//end if 18 | // browser monitoring works even when the PHP module isn't installed 19 | else 20 | { 21 | $this->browser(); 22 | } 23 | 24 | // WPCLI methods to exercise a site 25 | if ( defined( 'WP_CLI' ) && WP_CLI ) 26 | { 27 | $this->wpcli(); 28 | }//end if 29 | 30 | // init the last_timer object for use later 31 | $this->last_timer = (object) array(); 32 | }// END __construct 33 | 34 | /** 35 | * an object accessor for the browser object 36 | */ 37 | public function browser() 38 | { 39 | if ( ! $this->browser ) 40 | { 41 | require_once __DIR__ . '/class-go-newrelic-browser.php'; 42 | $this->browser = new GO_NewRelic_Browser(); 43 | }// end if 44 | 45 | return $this->browser; 46 | } // END browser 47 | 48 | /** 49 | * an object accessor for the apm object 50 | */ 51 | public function apm() 52 | { 53 | if ( ! $this->apm ) 54 | { 55 | require_once __DIR__ . '/class-go-newrelic-apm.php'; 56 | $this->apm = new GO_NewRelic_APM( $this ); 57 | }// end if 58 | 59 | return $this->apm; 60 | } // END apm 61 | 62 | /** 63 | * A loader for the WP:CLI class 64 | */ 65 | public function wpcli() 66 | { 67 | if ( $this->wpcli ) 68 | { 69 | return TRUE; 70 | } 71 | 72 | require_once __DIR__ . '/class-go-newrelic-wpcli.php'; 73 | 74 | // declare the class to WP:CLI 75 | WP_CLI::add_command( 'go-newrelic', 'GO_NewRelic_Wpcli' ); 76 | 77 | $this->wpcli = TRUE; 78 | }//end wpcli 79 | 80 | /** 81 | * returns our current configuration, or a value in the configuration. 82 | * 83 | * @param string $key (optional) key to a configuration value 84 | * @return mixed Returns the config array, or a config value if 85 | * $key is not NULL, or NULL if $key is specified but isn't set in 86 | * our config file. 87 | */ 88 | public function config( $key = NULL ) 89 | { 90 | if ( empty( $this->config ) ) 91 | { 92 | $this->config = apply_filters( 93 | 'go_config', 94 | // config array keys are the NR config names with sanitize_title_with_dashes() applied 95 | // and the `newrelic` prefix removed 96 | // see https://newrelic.com/docs/php/php-agent-phpini-settings for details 97 | array( 98 | 'license' => '', 99 | 'transaction-tracer-detail' => 0, 100 | 'newrelic-loglevel' => 'info', 101 | 'capture-params' => TRUE, 102 | 'ignored-params' => '', 103 | 'error-collector-enabled' => FALSE, 104 | ), 105 | 'go-newrelic' 106 | ); 107 | 108 | if ( empty( $this->config ) ) 109 | { 110 | do_action( 'go_slog', 'go-newrelic', 'Unable to load go-newrelic\' configuration' ); 111 | } 112 | }//END if 113 | 114 | if ( ! empty( $key ) ) 115 | { 116 | return isset( $this->config[ $key ] ) ? $this->config[ $key ] : NULL ; 117 | } 118 | 119 | return $this->config; 120 | }//END config 121 | 122 | 123 | /** 124 | * returns a name for the app based on context 125 | * 126 | * The dashboard, admin-ajax, cron, and front-end are all logged as separate apps. 127 | * This allows us to set different thresholds for those very different aspects of each site. 128 | * 129 | * @return string The appname 130 | */ 131 | public function get_appname() 132 | { 133 | // get the base app name from the home_url() 134 | $home_url = parse_url( home_url() ); 135 | $app_name = $home_url['host'] . ( isset( $home_url['path'] ) ? $home_url['path'] : '' ); 136 | 137 | // now get context 138 | if ( is_admin() ) 139 | { 140 | if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) 141 | { 142 | $app_name .= ' ajax'; 143 | }// END if 144 | else 145 | { 146 | $app_name .= ' admin'; 147 | }// END else 148 | }// end if 149 | elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) 150 | { 151 | $app_name .= ' cron'; 152 | }// END elseif 153 | 154 | return $app_name; 155 | }//END get_appname 156 | 157 | /** 158 | * A timer that can be used anywhere 159 | * 160 | * Inspired by some work and code by Mark Jaquith http://coveredwebservices.com/ 161 | */ 162 | public function timer( $name = '', $group = 'no group' ) 163 | { 164 | if ( ! isset( $this->last_timer->$group ) ) 165 | { 166 | $this->last_timer->$group = 0; 167 | } 168 | 169 | $current_timer = timer_stop( 0 ); 170 | $change = $current_timer - $this->last_timer->$group; 171 | $this->last_timer->$group = $current_timer; 172 | 173 | echo ''; 174 | }//END timer 175 | }//END class 176 | 177 | function go_newrelic() 178 | { 179 | global $go_newrelic; 180 | 181 | if ( ! isset( $go_newrelic ) || ! is_object( $go_newrelic ) ) 182 | { 183 | $go_newrelic = new GO_NewRelic(); 184 | }// END if 185 | 186 | return $go_newrelic; 187 | }// END go_newrelic 188 | -------------------------------------------------------------------------------- /components/class-go-newrelic-wpcli.php: -------------------------------------------------------------------------------- 1 | find_url( $url ); 57 | self::test_url( $assoc_args ); 58 | } 59 | }//end if 60 | else 61 | { 62 | $assoc_args['url'] = $this->find_url( $args[0] ); 63 | self::test_url( $assoc_args ); 64 | }//end else 65 | }//end exercise 66 | 67 | /** 68 | * Actually fetch the URL 69 | * 70 | * Given the args passed from exercise(). 71 | */ 72 | private function test_url( $args ) 73 | { 74 | $args = (object) wp_parse_args( $args, array( 75 | 'url' => NULL, 76 | 'count' => 11, 77 | 'redirection' => 0, 78 | 'rand' => FALSE, 79 | 'user_id' => FALSE, 80 | ) ); 81 | 82 | if ( ! $args->url ) 83 | { 84 | WP_CLI::warning( 'Empty URL, skipping.' ); 85 | return; 86 | } 87 | 88 | WP_CLI::line( "\n$args->url" ); 89 | if ( $args->rand ) 90 | { 91 | WP_CLI::line( 'URL will include randomized get vars to (maybe) break caching' ); 92 | } 93 | $cookies = array(); 94 | if ( $args->user_id ) 95 | { 96 | $cookies = $this->get_auth_cookies( $args->user_id ); 97 | WP_CLI::line( 'Using cookies from user ID ' . $args->user_id ); 98 | } 99 | WP_CLI::line( "Response\nCode\tSize\tTime\tCookies\tLast Modified\t\t\tCache Control\t\t\tCanonical" ); 100 | 101 | $runs = array(); 102 | for ( $i = 1; $i <= $args->count; $i++ ) 103 | { 104 | // the URL we're testing now 105 | if ( $args->rand ) 106 | { 107 | $test_url = add_query_arg( array( 'go-newrelic-exercise' => rand() ), $args->url ); 108 | } 109 | else 110 | { 111 | $test_url = $args->url; 112 | } 113 | 114 | // init this stat run 115 | $runs[ $i ] = (object) array( 'request_url' => $test_url ); 116 | 117 | $start_time = microtime( TRUE ); 118 | $fetch_raw = wp_remote_get( $test_url, array( 119 | 'timeout' => 90, 120 | 'redirection' => absint( $args->redirection ), 121 | 'headers' => array( 'x-go-newrelic-exercise' => rand() ), 122 | 'cookies' => $cookies, 123 | 'user-agent' => 'go-newrelic WordPress exerciser', 124 | 'sslverify' => FALSE, // this would be hugely insecure if we were doing anything with the data returned, but since this is used for testing (often against local hosts with self-signed certs).... 125 | ) ); 126 | 127 | // time the request 128 | $runs[ $i ]->response_time = microtime( TRUE ) - $start_time; 129 | 130 | // get the size 131 | $runs[ $i ]->response_size = strlen( wp_remote_retrieve_body( $fetch_raw ) ); 132 | 133 | // get the response code 134 | $runs[ $i ]->response_code = wp_remote_retrieve_response_code( $fetch_raw ); 135 | 136 | // get the count of any cookies 137 | $temp = wp_remote_retrieve_header( $fetch_raw, 'set-cookie' ); 138 | $runs[ $i ]->response_cookies = empty( $temp ) ? 0 : count( (array) $temp ); 139 | 140 | // the last modified header 141 | $temp = wp_remote_retrieve_header( $fetch_raw, 'last-modified' ); 142 | $runs[ $i ]->response_modified = is_array( $temp ) ? 'WARNING, ' . count( $temp ) .' headers found' : empty( $temp ) ? "Null response\t\t" : $temp; 143 | 144 | // the cache control header 145 | $temp = wp_remote_retrieve_header( $fetch_raw, 'cache-control' ); 146 | $runs[ $i ]->response_cachecontrol = is_array( $temp ) ? 'WARNING, ' . count( $temp ) .' headers found' : empty( $temp ) ? "Null response\t\t" : $temp; 147 | 148 | // canonical or redirect? 149 | $runs[ $i ]->response_canonical = 'Null response'; // default value, override if another is found 150 | $temp = wp_remote_retrieve_header( $fetch_raw, 'location' ); 151 | if ( 152 | ! empty( $temp ) && 153 | ! is_array( $temp ) 154 | ) 155 | { 156 | $runs[ $i ]->response_canonical = $this->find_url( $temp ); 157 | }//end if 158 | else 159 | { 160 | preg_match_all( '#]+)(/>|>)#is', wp_remote_retrieve_body( $fetch_raw ), $matches ); 161 | foreach ( $matches[1] as $temp ) 162 | { 163 | if ( preg_match( '#rel\s?=[^=]*canonical#is', $temp ) ) 164 | { 165 | $runs[ $i ]->response_canonical = $this->find_url( $temp ); 166 | break; 167 | } 168 | } 169 | }//end else 170 | 171 | WP_CLI::line( sprintf( 172 | "%d\t%sK\t%s\t%d\t%s\t%s\t%s", 173 | $runs[ $i ]->response_code, 174 | number_format( $runs[ $i ]->response_size / 1024, 1 ), 175 | number_format( $runs[ $i ]->response_time, 2 ), 176 | $runs[ $i ]->response_cookies, 177 | $runs[ $i ]->response_modified, 178 | $runs[ $i ]->response_cachecontrol, 179 | $runs[ $i ]->response_canonical 180 | ) ); 181 | }//end for 182 | 183 | // indeiscriminately attempt to clear the session 184 | // prevents accumulation of sessions in user_meta 185 | $this->clear_auth_session( $args->user_id ); 186 | }//end test_url 187 | 188 | /** 189 | * Extract what looks like a URL from unstructured text 190 | * 191 | * Used when reading headers, not for parsing user input. 192 | */ 193 | private function find_url( $text ) 194 | { 195 | // nice regex thanks to John Gruber http://daringfireball.net/2010/07/improved_regex_for_matching_urls 196 | preg_match_all( '#(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))#', $text, $urls ); 197 | 198 | if ( ! isset( $urls[0][0] ) ) 199 | { 200 | return NULL; 201 | } 202 | 203 | return $urls[0][0]; 204 | }//end find_url 205 | 206 | /** 207 | * Get auth cookies and start a session for a user 208 | * 209 | * This is not the security vulerability you think it is: 210 | * 1. anybody with access to WP:CLI can execute commands on behalf of a user without knowing the password 211 | * 2. the session is destroyed when done, so the cookie becomes invalid and useless if intercepted 212 | */ 213 | private function get_auth_cookies( $user_id ) 214 | { 215 | $expiration = time() + DAY_IN_SECONDS; 216 | 217 | require_once( ABSPATH . WPINC . '/session.php' ); 218 | 219 | $manager = WP_Session_Tokens::get_instance( $user_id ); 220 | $this->token = $manager->create( $expiration ); 221 | 222 | return array( 223 | SECURE_AUTH_COOKIE => wp_generate_auth_cookie( $user_id, $expiration, 'secure_auth', $this->token ), 224 | AUTH_COOKIE => wp_generate_auth_cookie( $user_id, $expiration, 'auth', $this->token ), 225 | LOGGED_IN_COOKIE => wp_generate_auth_cookie( $user_id, $expiration, 'logged_in', $this->token ), 226 | ); 227 | }//end get_auth_cookies 228 | 229 | /** 230 | * Destroy the user session 231 | */ 232 | private function clear_auth_session( $user_id ) 233 | { 234 | if ( $this->token ) 235 | { 236 | $manager = WP_Session_Tokens::get_instance( $user_id ); 237 | $manager->destroy( $this->token ); 238 | } 239 | }//end clear_auth_session 240 | }//end class -------------------------------------------------------------------------------- /components/class-go-newrelic-browser.php: -------------------------------------------------------------------------------- 1 | settings = get_option( $this->slug . '-settings' ); 11 | 12 | if ( $this->settings ) 13 | { 14 | // New Relic claims we are not tracking because the code should occur before any other scripts in the head, let's move it up! 15 | add_action( 'wp_head', array( $this, 'output_browser_tracking_code' ), 0 ); 16 | // admin_print_scripts is more appropriate, but this needs to happed as soon as possible 17 | add_action( 'admin_enqueue_scripts', array( $this, 'output_browser_tracking_code' ), 0 ); 18 | }//end if 19 | 20 | add_action( 'admin_menu', array( $this, 'admin_menu' ) ); 21 | }// end __construct 22 | 23 | public function admin_menu() 24 | { 25 | add_options_page( $this->name . ' Settings', $this->name . ' Settings', 'manage_options', $this->slug . '-settings', array( $this, 'settings' ) ); 26 | }// end admin_menu 27 | 28 | public function output_browser_tracking_code() 29 | { 30 | if ( ! $this->is_enabled() ) 31 | { 32 | return; 33 | }//end if 34 | ?> 35 | 39 | slug . '-settings' == $_GET['page'] 52 | && wp_verify_nonce( $_POST[ $this->slug . '-nonce' ], plugin_basename( __FILE__ ) ) 53 | ) 54 | { 55 | $this->enable( $_POST['newrelic-enable'] ); 56 | if ( ! empty( $_POST['go-newrelic-script'] ) ) 57 | { 58 | $this->update_settings( $_POST['go-newrelic-script'] ); 59 | } 60 | }//end if 61 | 62 | ?> 63 |