529 lines
19 KiB
PHP
529 lines
19 KiB
PHP
<?php
|
|
define( 'NEWRELIC_API_BASE', 'https://api.newrelic.com' );
|
|
|
|
/**
|
|
* Interacts with the New Relic REST API v2.
|
|
*
|
|
* deprecated
|
|
*
|
|
* @link https://docs.newrelic.com/docs/apis/rest-api-v2/get-started/introduction-new-relic-rest-api-v2/
|
|
*/
|
|
class NewRelicAPI {
|
|
/**
|
|
* API key used for authorization.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $_api_key;
|
|
|
|
/**
|
|
* Cached account id from any application response.
|
|
*
|
|
* @var int|null
|
|
*/
|
|
private $account_id_cache = null;
|
|
|
|
/**
|
|
* NerdGraph endpoint for account discovery.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $nerdgraph_endpoint = 'https://api.newrelic.com/graphql';
|
|
|
|
/**
|
|
* @param string $api_key New Relic API Key.
|
|
*/
|
|
public function __construct( $api_key ) {
|
|
$this->_api_key = $api_key;
|
|
}
|
|
|
|
/**
|
|
* Debug helper with fallback when Util_Debug isn't loaded yet.
|
|
*
|
|
* @param string $message Message.
|
|
* @return void
|
|
*/
|
|
private function log_debug( $message ) {
|
|
// no-op (debug logging disabled).
|
|
}
|
|
|
|
/**
|
|
* Build request URL including query parameters.
|
|
*
|
|
* @param string $api_call_url API path.
|
|
* @param array $query Optional query parameters.
|
|
* @return string
|
|
*/
|
|
private function build_url( $api_call_url, $query = array() ) {
|
|
$url = NEWRELIC_API_BASE . $api_call_url;
|
|
if ( ! empty( $query ) ) {
|
|
$url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . http_build_query( $query, '', '&', PHP_QUERY_RFC3986 );
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
/**
|
|
* @param string $api_call_url url path with query string used to define what to get from the NR API.
|
|
* @param array $query Optional query parameters.
|
|
* @throws Exception If the request fails.
|
|
* @return string
|
|
*/
|
|
private function _get( $api_call_url, $query = array() ) {
|
|
$url = $this->build_url( $api_call_url, $query );
|
|
$defaults = array(
|
|
'headers' => array(
|
|
'X-Api-Key' => $this->_api_key,
|
|
),
|
|
'timeout' => 5,
|
|
);
|
|
|
|
$start = microtime( true );
|
|
$this->log_debug( sprintf( 'GET %s start', $url ) );
|
|
|
|
$response = wp_remote_get( $url, $defaults );
|
|
$elapsed = round( ( microtime( true ) - $start ) * 1000 );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
$this->log_debug( sprintf( 'GET %s error (%d ms): %s', $url, $elapsed, $response->get_error_message() ) );
|
|
throw new Exception( 'Could not get data' );
|
|
} elseif ( 200 === (int) $response['response']['code'] ) {
|
|
$this->log_debug( sprintf( 'GET %s success (%d ms)', $url, $elapsed ) );
|
|
return $response['body'];
|
|
}
|
|
|
|
switch ( (string) $response['response']['code'] ) {
|
|
case '403':
|
|
$message = __( 'Invalid API key', 'w3-total-cache' );
|
|
break;
|
|
default:
|
|
$message = $response['response']['message'];
|
|
}
|
|
|
|
$body_snippet = isset( $response['body'] ) ? substr( $response['body'], 0, 500 ) : '';
|
|
$this->log_debug( sprintf( 'GET %s failed (%d ms): %s %s Body: %s', $url, $elapsed, $response['response']['code'], $message, $body_snippet ) );
|
|
throw new Exception( $message, $response['response']['code'] );
|
|
}
|
|
|
|
/**
|
|
* @param string $api_call_url url path with query string used to define what to get from the NR API.
|
|
* @param array $params key value array.
|
|
* @throws Exception If the request fails.
|
|
* @return bool
|
|
*/
|
|
private function _put( $api_call_url, $params ) {
|
|
$url = $this->build_url( $api_call_url );
|
|
$defaults = array(
|
|
'method' => 'PUT',
|
|
'headers' => array(
|
|
'X-Api-Key' => $this->_api_key,
|
|
),
|
|
'body' => $params,
|
|
'timeout' => 5,
|
|
);
|
|
$start = microtime( true );
|
|
$this->log_debug( sprintf( 'PUT %s start', $url ) );
|
|
|
|
$response = wp_remote_request( $url, $defaults );
|
|
$elapsed = round( ( microtime( true ) - $start ) * 1000 );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
$this->log_debug( sprintf( 'PUT %s error (%d ms): %s', $url, $elapsed, $response->get_error_message() ) );
|
|
throw new Exception( 'Could not put data' );
|
|
} elseif ( in_array( (int) $response['response']['code'], array( 200, 201 ), true ) ) {
|
|
$this->log_debug( sprintf( 'PUT %s success (%d ms)', $url, $elapsed ) );
|
|
return true;
|
|
}
|
|
|
|
$this->log_debug( sprintf( 'PUT %s failed (%d ms): %s %s', $url, $elapsed, $response['response']['code'], $response['response']['message'] ) );
|
|
throw new Exception( $response['response']['message'], $response['response']['code'] );
|
|
}
|
|
|
|
/**
|
|
* Decode JSON response and guard against unexpected payloads.
|
|
*
|
|
* @param string $response Raw response string.
|
|
* @return array
|
|
* @throws Exception If JSON is invalid.
|
|
*/
|
|
private function decode_response( $response ) {
|
|
$decoded = json_decode( $response, true );
|
|
if ( null === $decoded && JSON_ERROR_NONE !== json_last_error() ) {
|
|
throw new Exception( 'Received unexpected response' );
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
/**
|
|
* Get applications connected with the API key.
|
|
*
|
|
* @param int $account_id Deprecated.
|
|
* @return array
|
|
*/
|
|
public function get_applications( $account_id ) {
|
|
$applications = array();
|
|
$data = $this->decode_response( $this->_get( '/v2/applications.json' ) );
|
|
|
|
if ( isset( $data['applications'] ) && is_array( $data['applications'] ) ) {
|
|
$this->log_debug( '[get_applications] received ' . count( $data['applications'] ) . ' apps' );
|
|
foreach ( $data['applications'] as $application ) {
|
|
if ( isset( $application['id'], $application['name'] ) ) {
|
|
$applications[ (int) $application['id'] ] = $application['name'];
|
|
}
|
|
|
|
if ( isset( $application['account_id'] ) && null === $this->account_id_cache ) {
|
|
$this->account_id_cache = (int) $application['account_id'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $applications;
|
|
}
|
|
|
|
/**
|
|
* Get the application summary data for the provided application.
|
|
*
|
|
* @param int $account_id Deprecated.
|
|
* @param int $application_id Application ID.
|
|
* @return array array(metric name => metric value)
|
|
*/
|
|
public function get_application_summary( $account_id, $application_id ) {
|
|
$summary = array();
|
|
$data = $this->decode_response( $this->_get( "/v2/applications/{$application_id}.json" ) );
|
|
|
|
if ( empty( $data['application'] ) ) {
|
|
return $summary;
|
|
}
|
|
|
|
$application = $data['application'];
|
|
|
|
if ( isset( $application['application_summary'] ) && is_array( $application['application_summary'] ) ) {
|
|
$app_summary = $application['application_summary'];
|
|
if ( isset( $app_summary['apdex_score'] ) ) {
|
|
$summary['Apdex'] = $app_summary['apdex_score'];
|
|
}
|
|
if ( isset( $app_summary['error_rate'] ) ) {
|
|
$summary['Error Rate'] = $app_summary['error_rate'];
|
|
}
|
|
if ( isset( $app_summary['throughput'] ) ) {
|
|
$summary['Throughput'] = $app_summary['throughput'];
|
|
}
|
|
if ( isset( $app_summary['response_time'] ) ) {
|
|
$summary['Response Time'] = $app_summary['response_time'];
|
|
}
|
|
}
|
|
|
|
if ( isset( $application['end_user_summary']['response_time'] ) ) {
|
|
$summary['Application Busy'] = $application['end_user_summary']['response_time'];
|
|
}
|
|
|
|
// Fetch additional metrics for DB / CPU / Memory.
|
|
$extra_metrics = array(
|
|
// Use Datastore/all for DB; fall back to average_value if response time is absent.
|
|
'DB' => array(
|
|
'name' => 'Datastore/all',
|
|
'field' => 'average_response_time',
|
|
'fallback_field' => 'average_value',
|
|
),
|
|
'CPU' => array( 'name' => 'CPU/User Time', 'field' => 'average_value' ),
|
|
'Memory' => array( 'name' => 'Memory/Physical', 'field' => 'average_value' ),
|
|
);
|
|
|
|
foreach ( $extra_metrics as $label => $meta ) {
|
|
$metric_data = $this->get_metric_data(
|
|
0,
|
|
$application_id,
|
|
gmdate( 'Y-m-d\TH:i:s\Z', strtotime( '-1 day' ) ),
|
|
gmdate( 'Y-m-d\TH:i:s\Z' ),
|
|
array( $meta['name'] ),
|
|
$meta['field'],
|
|
true
|
|
);
|
|
|
|
if ( ! empty( $metric_data ) ) {
|
|
$first = reset( $metric_data );
|
|
$value = null;
|
|
if ( isset( $first->{$meta['field']} ) && '' !== $first->{$meta['field']} ) {
|
|
$value = $first->{$meta['field']};
|
|
} elseif ( isset( $meta['fallback_field'] ) && isset( $first->{$meta['fallback_field']} ) ) {
|
|
$value = $first->{$meta['fallback_field']};
|
|
}
|
|
|
|
if ( null !== $value ) {
|
|
$summary[ $label ] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* Get a single application with all attributes.
|
|
*
|
|
* @param int $application_id Application ID.
|
|
* @return array|null
|
|
*/
|
|
public function get_application( $application_id ) {
|
|
$data = $this->decode_response( $this->_get( "/v2/applications/{$application_id}.json" ) );
|
|
if ( isset( $data['application'] ) && is_array( $data['application'] ) ) {
|
|
if ( isset( $data['application']['account_id'] ) ) {
|
|
$this->account_id_cache = (int) $data['application']['account_id'];
|
|
}
|
|
$this->log_debug( '[get_application] app id ' . $application_id . ' payload: ' . wp_json_encode( $data['application'] ) );
|
|
return $data['application'];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Attempt to fetch the first account via NerdGraph (GraphQL) API.
|
|
*
|
|
* @return int|null
|
|
*/
|
|
private function get_account_from_nerdgraph() {
|
|
$query = '{ actor { accounts { id name } } }';
|
|
$body = wp_json_encode( array( 'query' => $query ) );
|
|
$args = array(
|
|
'method' => 'POST',
|
|
'headers' => array(
|
|
'API-Key' => $this->_api_key,
|
|
'Content-Type' => 'application/json',
|
|
),
|
|
'body' => $body,
|
|
'timeout' => 5,
|
|
);
|
|
|
|
$this->log_debug( '[nerdgraph] POST ' . $this->nerdgraph_endpoint . ' start' );
|
|
$response = wp_remote_post( $this->nerdgraph_endpoint, $args );
|
|
if ( is_wp_error( $response ) ) {
|
|
$this->log_debug( '[nerdgraph] error: ' . $response->get_error_message() );
|
|
return null;
|
|
}
|
|
|
|
$code = isset( $response['response']['code'] ) ? (int) $response['response']['code'] : 0;
|
|
$this->log_debug( '[nerdgraph] response code ' . $code );
|
|
if ( 200 !== $code ) {
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode( $response['body'], true );
|
|
if ( isset( $decoded['data']['actor']['accounts'][0]['id'] ) ) {
|
|
$account_id = (int) $decoded['data']['actor']['accounts'][0]['id'];
|
|
$this->log_debug( '[nerdgraph] resolved account_id=' . $account_id );
|
|
return $account_id;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return key value array with information connected to account.
|
|
*
|
|
* @return array|mixed|null
|
|
*/
|
|
public function get_account() {
|
|
static $account_cache = null;
|
|
|
|
if ( null !== $account_cache ) {
|
|
return $account_cache;
|
|
}
|
|
|
|
// First try NerdGraph for a reliable account id.
|
|
$ng_account_id = $this->get_account_from_nerdgraph();
|
|
if ( $ng_account_id ) {
|
|
$account_cache = array(
|
|
'id' => $ng_account_id,
|
|
'subscription' => array(
|
|
'product-name' => 'Standard',
|
|
),
|
|
'license-key' => null,
|
|
);
|
|
$this->account_id_cache = $ng_account_id;
|
|
return $account_cache;
|
|
}
|
|
|
|
// Derive account data from the first application (accounts endpoint not reliable).
|
|
$data = $this->decode_response( $this->_get( '/v2/applications.json' ) );
|
|
|
|
if ( isset( $data['applications'][0] ) ) {
|
|
$application = $data['applications'][0];
|
|
$account_cache = array(
|
|
'id' => isset( $application['account_id'] ) ? (int) $application['account_id'] : $this->account_id_cache,
|
|
'subscription' => array(
|
|
'product-name' => 'Standard',
|
|
),
|
|
'license-key' => isset( $application['license_key'] ) ? $application['license_key'] : null,
|
|
);
|
|
$this->log_debug( '[get_account] derived account_id=' . $account_cache['id'] . ' from application payload.' );
|
|
} elseif ( isset( $this->account_id_cache ) ) {
|
|
$account_cache = array(
|
|
'id' => $this->account_id_cache,
|
|
'subscription' => array(
|
|
'product-name' => 'Standard',
|
|
),
|
|
'license-key' => null,
|
|
);
|
|
}
|
|
|
|
if ( is_null( $account_cache ) ) {
|
|
$this->log_debug( '[get_account] Unable to derive account_id from applications payload.' );
|
|
if ( ! empty( $data['applications'] ) ) {
|
|
$sample = $data['applications'][0];
|
|
$this->log_debug( '[get_account] Sample application payload: ' . wp_json_encode( $sample ) );
|
|
}
|
|
}
|
|
|
|
return $account_cache;
|
|
}
|
|
|
|
/**
|
|
* Get key value array with application settings.
|
|
*
|
|
* @param int $account_id Deprecated.
|
|
* @param int $application_id Application ID.
|
|
* @return array|mixed
|
|
*/
|
|
public function get_application_settings( $account_id, $application_id ) {
|
|
$data = $this->decode_response( $this->_get( "/v2/applications/{$application_id}.json" ) );
|
|
$settings = array();
|
|
$app_block = isset( $data['application'] ) ? $data['application'] : array();
|
|
|
|
if ( isset( $app_block['settings'] ) ) {
|
|
$settings = $app_block['settings'];
|
|
}
|
|
|
|
// Normalize to expected keys used by the view.
|
|
$normalized = array(
|
|
'application-id' => isset( $app_block['id'] ) ? $app_block['id'] : '',
|
|
'name' => isset( $app_block['name'] ) ? $app_block['name'] : '',
|
|
'alerts-enabled' => isset( $settings['use_server_side_config'] ) ? ( $settings['use_server_side_config'] ? 'true' : 'false' ) : 'false',
|
|
'app-apdex-t' => isset( $settings['app_apdex_threshold'] ) ? $settings['app_apdex_threshold'] : '',
|
|
'rum-apdex-t' => isset( $settings['end_user_apdex_threshold'] ) ? $settings['end_user_apdex_threshold'] : '',
|
|
'rum-enabled' => isset( $settings['enable_real_user_monitoring'] ) ? ( $settings['enable_real_user_monitoring'] ? 'true' : 'false' ) : 'false',
|
|
);
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/**
|
|
* Update application settings. verifies the keys in provided settings array is acceptable.
|
|
*
|
|
* @param int $account_id Deprecated.
|
|
* @param int $application_id Application ID.
|
|
* @param array $settings Settings to update.
|
|
* @return bool
|
|
*/
|
|
public function update_application_settings( $account_id, $application_id, $settings ) {
|
|
$supported = array( 'alerts_enabled', 'app_apdex_t', 'rum_apdex_t', 'rum_enabled' );
|
|
$call = "/v2/applications/{$application_id}.json";
|
|
$params = array();
|
|
|
|
foreach ( $settings as $key => $value ) {
|
|
if ( in_array( $key, $supported, true ) ) {
|
|
$params[ $key ] = $value;
|
|
}
|
|
}
|
|
|
|
$payload = array(
|
|
'application' => array(
|
|
'settings' => $params,
|
|
),
|
|
);
|
|
|
|
return $this->_put( $call, $payload );
|
|
}
|
|
|
|
/**
|
|
* Returns the available metric names for provided application.
|
|
*
|
|
* @param int $application_id Application ID.
|
|
* @param string $regex Optional regex to filter metric names.
|
|
* @param string $limit Optional limit (not used, kept for BC).
|
|
* @return array|mixed
|
|
*/
|
|
public function get_metric_names( $application_id, $regex = '', $limit = '' ) {
|
|
$data = $this->decode_response( $this->_get( "/v2/applications/{$application_id}/metrics.json" ) );
|
|
$metrics = array();
|
|
|
|
if ( isset( $data['metrics'] ) && is_array( $data['metrics'] ) ) {
|
|
foreach ( $data['metrics'] as $metric ) {
|
|
if ( ! isset( $metric['name'] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$name = $metric['name'];
|
|
|
|
if ( $regex && ! @preg_match( '/' . str_replace( '/', '\/', $regex ) . '/', $name ) ) {
|
|
continue;
|
|
}
|
|
|
|
$metrics[] = (object) array(
|
|
'name' => $name,
|
|
);
|
|
}
|
|
}
|
|
|
|
return $metrics;
|
|
}
|
|
|
|
/**
|
|
* Gets the metric data for the provided metric names.
|
|
*
|
|
* @param string $account_id Deprecated.
|
|
* @param string $application_id Application ID.
|
|
* @param string $begin XML date in GMT.
|
|
* @param string $to XML date in GMT.
|
|
* @param array $metrics Metric names.
|
|
* @param string $field Field to retrieve.
|
|
* @param bool $summary If values should be merged or overtime.
|
|
* @return array|mixed
|
|
*/
|
|
public function get_metric_data( $account_id, $application_id, $begin, $to, $metrics, $field, $summary = true ) {
|
|
if ( empty( $metrics ) ) {
|
|
return array();
|
|
}
|
|
|
|
$metrics = is_array( $metrics ) ? $metrics : array( $metrics );
|
|
|
|
// Manually build query string to satisfy NR format: names[]=...&values[]=...
|
|
$parts = array(
|
|
'from=' . rawurlencode( $begin ),
|
|
'to=' . rawurlencode( $to ),
|
|
'summarize=' . ( $summary ? 'true' : 'false' ),
|
|
);
|
|
foreach ( $metrics as $name ) {
|
|
$parts[] = 'names[]=' . rawurlencode( $name );
|
|
}
|
|
$parts[] = 'values[]=' . rawurlencode( $field );
|
|
|
|
$url = "/v2/applications/{$application_id}/metrics/data.json?" . implode( '&', $parts );
|
|
$data = $this->decode_response(
|
|
$this->_get( $url )
|
|
);
|
|
|
|
if ( ! isset( $data['metric_data']['metrics'] ) || ! is_array( $data['metric_data']['metrics'] ) ) {
|
|
return array();
|
|
}
|
|
|
|
$metric_data = array();
|
|
foreach ( $data['metric_data']['metrics'] as $metric ) {
|
|
$formatted = array(
|
|
'name' => isset( $metric['name'] ) ? $metric['name'] : '',
|
|
);
|
|
|
|
if ( isset( $metric['timeslices'][0]['values'] ) && is_array( $metric['timeslices'][0]['values'] ) ) {
|
|
foreach ( $metric['timeslices'][0]['values'] as $key => $value ) {
|
|
$formatted[ $key ] = $value;
|
|
}
|
|
}
|
|
|
|
$metric_data[] = (object) $formatted;
|
|
}
|
|
|
|
return $metric_data;
|
|
}
|
|
}
|