| <?php |
| /** |
| * Copyright 2007 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| /** |
| */ |
| |
| namespace google\appengine\api\log; |
| |
| use google\appengine\LogReadRequest; |
| use google\appengine\LogReadResponse; |
| use google\appengine\LogServiceError\ErrorCode; |
| use google\appengine\runtime\ApiProxy; |
| use google\appengine\runtime\ApplicationError; |
| use google\appengine\util\StringUtil; |
| |
| /** |
| * The LogService allows an application to query for request and application |
| * logs. Application logs are added to a the current request log by calling |
| * @link http://php.net/manual/en/function.syslog.php syslog(int $priority, |
| * string $message). The $priority used when creating the application log is |
| * translated into a different scale of severity used by the LogService. |
| * |
| * Application logs have a level in order of increasing severity: |
| * <table> |
| * <tr><th>syslog $priority</th><th>GAE severity</th></tr> |
| * <tr><td>LOG_DEBUG</td><td>LogService::LEVEL_DEBUG</td></tr> |
| * <tr><td>LOG_INFO</td><td>LogService::LEVEL_INFO</td></tr> |
| * <tr><td>LOG_NOTICE</td><td>LogService::LEVEL_INFO</td></tr> |
| * <tr><td>LOG_WARNING</td><td>LogService::LEVEL_WARNING</td></tr> |
| * <tr><td>LOG_ERR</td><td>LogService::LEVEL_ERROR</td></tr> |
| * <tr><td>LOG_CRIT</td><td>LogService::LEVEL_CRITICAL</td></tr> |
| * <tr><td>LOG_ALERT</td><td>LogService::LEVEL_CRITICAL</td></tr> |
| * <tr><td>LOG_EMERG</td><td>LogService::LEVEL_CRITICAL</td></tr> |
| * <table> |
| * |
| * When fetching application logs or filtering request logs by severity use the |
| * values in the right hand column. |
| */ |
| final class LogService { |
| use ApiProxyAccess; |
| |
| /** |
| * Constants for application log levels. |
| */ |
| const LEVEL_DEBUG = 0; |
| const LEVEL_INFO = 1; |
| const LEVEL_WARNING = 2; |
| const LEVEL_ERROR = 3; |
| const LEVEL_CRITICAL = 4; |
| |
| /** |
| * The maximum number of request logs returned in each batch. |
| */ |
| const MAX_BATCH_SIZE = 1000; |
| |
| // Map syslog priority levels to appengine severity levels. |
| private static $syslog_priority_map = array( |
| LOG_EMERG => self::LEVEL_CRITICAL, |
| LOG_ALERT => self::LEVEL_CRITICAL, |
| LOG_CRIT => self::LEVEL_CRITICAL, |
| LOG_ERR => self::LEVEL_ERROR, |
| LOG_WARNING => self::LEVEL_WARNING, |
| LOG_NOTICE => self::LEVEL_INFO, |
| LOG_INFO => self::LEVEL_INFO, |
| LOG_DEBUG => self::LEVEL_DEBUG); |
| |
| |
| // Validation patterns copied from google/appengine/api/logservice/logservice.py |
| private static $MAJOR_VERSION_ID_REGEX = |
| '/^(?:(?:((?!-)[a-z\d\-]{1,63}):)?)((?!-)[a-z\d\-]{1,100})$/'; |
| private static $REQUEST_ID_REGEX = '/^[\da-fA-F]+$/'; |
| |
| /** |
| * Get request logs matching the given options in reverse chronological |
| * order of request end time. |
| * |
| * @param array $options Optional associateive arrary of filters and |
| * modifiers from following: |
| * |
| * <ul> |
| * <li>'start_time': <code>DateTime or numeric</code> The earliest |
| * completion time or last-update time for request logs. If the value is |
| * numeric it represents microseconds since Unix epoch.</li> |
| * <li>'end_time': <code>DateTime or numeric</code> The latest completion |
| * time or last-update time for request logs. If the value is numeric it |
| * represents microseconds since Unix epoch.</li> |
| * <li>'offset': <code>string</code> The url-safe offset value from a |
| * <code>RequestLog</code> to continue iterating after.</li> |
| * <li>'minimum_log_level': <code>integer</code> Only return request logs |
| * containing at least one application log of this severity or higher. |
| * Works even if include_app_logs is not <code>true</code></li> |
| * <li>'include_incomplete': <code>boolean</code> Should incomplete request |
| * logs be included. The default is <code>false</code> - only completed |
| * logs are returned</li> |
| * <li>'include_app_logs': <code>boolean</code> Should application logs be |
| * returned. The default is <code>false</code> - application logs are not |
| * returned with their containing request logs.</li> |
| * <li>'versions': <code>array</code> The versions of the default module |
| * for which to fetch request logs. Only one of 'versions' and |
| * 'module_versions' can be used.</li> |
| * <li>'module_versions': <code>arrary/code> An associative array of module |
| * names to versions for which to fetch request logs. Each module name may |
| * be mapped to either a single <code>string</code> version or an <code> |
| * array</code> of versions.</li> |
| * <li>'batch_size': <code>integer</code> The number of request logs to |
| * pre-fetch while iterating.</li> |
| * </ul> |
| * |
| * @return Iterator The matching <code>RequestLog</code> items. |
| */ |
| public static function fetch(array $options = []) { |
| $request = new LogReadRequest(); |
| $request->setAppId(getenv('APPLICATION_ID')); |
| |
| // Required options default values - overridden by options below. |
| $batch_size = 20; |
| $include_app_logs = false; |
| $include_incomplete = false; |
| |
| foreach ($options as $key => $value) { |
| switch ($key) { |
| case 'start_time': |
| if (is_numeric($value)) { |
| $usec = (double) $value; |
| } else if ($value instanceof \DateTime) { |
| $usec = self::dateTimeToUsecs($value); |
| } else { |
| self::optionTypeException($key, $value, 'DateTime or numeric'); |
| } |
| $request->setStartTime($usec); |
| break; |
| case 'end_time': |
| if (is_numeric($value)) { |
| $usec = (double) $value; |
| } else if ($value instanceof \DateTime) { |
| $usec = self::dateTimeToUsecs($value); |
| } else { |
| self::optionTypeException($key, $value, 'DateTime or numeric'); |
| } |
| $request->setEndTime($usec); |
| break; |
| case 'offset': |
| if (!is_string($value)) { |
| self::optionTypeException($key, $value, 'string'); |
| } |
| $decoded = StringUtil::base64UrlDecode($value); |
| $request->mutableOffset()->parseFromString($decoded); |
| break; |
| case 'minimum_log_level': |
| if (!is_int($value)) { |
| self::optionTypeException($key, $value, 'integer'); |
| } |
| if ($value > self::LEVEL_CRITICAL || |
| $value < self::LEVEL_DEBUG) { |
| throw new \InvalidArgumentException( |
| "Option 'minimum_log_level' must be from " . |
| self::LEVEL_DEBUG . " to " . |
| self::LEVEL_CRITICAL); |
| } |
| $request->setMinimumLogLevel($value); |
| break; |
| case 'include_incomplete': |
| if (!is_bool($value)) { |
| self::optionTypeException($key, $value, 'boolean'); |
| } |
| $include_incomplete = $value; |
| break; |
| case 'include_app_logs': |
| if (!is_bool($value)) { |
| self::optionTypeException($key, $value, 'boolean'); |
| } |
| $include_app_logs = $value; |
| break; |
| case 'module_versions': |
| if (!is_array($value)) { |
| self::optionTypeException($key, $value, 'array'); |
| } |
| if (isset($options['versions'])) { |
| throw new \InvalidArgumentException( |
| "Only one of 'versions' or " . |
| "'module_versions' may be set"); |
| } |
| foreach ($value as $module => $versions) { |
| if (!is_string($module)) { |
| throw new \InvalidArgumentException( |
| 'Server must be a string but was ' . |
| self::typeOrClass($module)); |
| } |
| // Versions can be a single string or an array of strings. |
| if (is_array($versions)) { |
| foreach ($versions as $version) { |
| if (!is_string($version)) { |
| throw new \InvalidArgumentException( |
| 'Server version must be a string but was ' . |
| self::typeOrClass($version)); |
| } |
| $module_version = $request->addModuleVersion(); |
| if ($module !== 'default') { |
| $module_version->setModuleId($module); |
| } |
| $module_version->setVersionId($version); |
| } |
| } else if (is_string($versions)) { |
| $module_version = $request->addModuleVersion(); |
| $module_version->setModuleId($module); |
| $module_version->setVersionId($versions); |
| } else { |
| throw new \InvalidArgumentException( |
| 'Server version must be a string or array but was ' . |
| self::typeOrClass($versions)); |
| } |
| } |
| break; |
| case 'versions': |
| if (!is_array($value)) { |
| self::optionTypeException($key, $value, 'array'); |
| } |
| if (isset($options['module_versions'])) { |
| throw new \InvalidArgumentException( |
| "Only one of 'versions' or " . |
| "'module_versions' may be set"); |
| } |
| foreach ($value as $version) { |
| if (!is_string($version)) { |
| throw new \InvalidArgumentException( |
| 'Version must be a string but was ' . |
| self::typeOrClass($version)); |
| } |
| if (!preg_match(self::$MAJOR_VERSION_ID_REGEX, $version)) { |
| throw new \InvalidArgumentException( |
| "Invalid version id " . htmlspecialchars($version)); |
| } |
| $request->addModuleVersion()->setVersionId($version); |
| } |
| break; |
| case 'batch_size': |
| if (!is_int($value)) { |
| self::optionTypeException($key, $value, 'integer'); |
| } |
| if ($value > self::MAX_BATCH_SIZE || $value < 1) { |
| throw new \InvalidArgumentException( |
| 'Batch size must be > 0 and <= ' . self::MAX_BATCH_SIZE); |
| } |
| $batch_size = $value; |
| break; |
| default: |
| throw new \InvalidArgumentException( |
| "Invalid option " . htmlspecialchars($key)); |
| } |
| } |
| |
| // Set required options. |
| $request->setIncludeIncomplete($include_incomplete); |
| $request->setIncludeAppLogs($include_app_logs); |
| $request->setCount($batch_size); |
| |
| // Set version to the current version if none set explicitly. |
| if ($request->getModuleVersionSize() === 0) { |
| self::setDefaultModuleVersion($request); |
| } |
| |
| return new RequestLogIterator($request); |
| } |
| |
| private static function setDefaultModuleVersion($request) { |
| $mv = $request->addModuleVersion(); |
| $current_module = getenv('CURRENT_MODULE_ID'); |
| if ($current_module !== 'default') { |
| $mv->setModuleId($current_module); |
| } |
| $current_version = getenv('CURRENT_VERSION_ID'); |
| $whole_version = explode('.', $current_version)[0]; |
| $mv->setVersionId($whole_version); |
| } |
| |
| /** |
| * Get request logs for the given request log ids and optionally include the |
| * application logs addded during each request. Request log ids that are not |
| * found are ignored so the returned array may have fewer items than |
| * <code>$request_ids</code>. |
| * |
| * @param mixed $request_ids A string request id or an array of string request |
| * ids obtained from <code>RequestLog::getRequestId()</code>. |
| * @param boolean $include_app_logs Should applicaiton logs be included in the |
| * fetched request logs. Defaults to true - application logs are included. |
| * |
| * @return RequestLog[] The request logs for ids that were found. |
| */ |
| public static function fetchById($request_ids, $include_app_logs = true) { |
| $request = new LogReadRequest(); |
| $request->setAppId(getenv('APPLICATION_ID')); |
| |
| if (!is_bool($include_app_logs)) { |
| throw new \InvalidArgumentException( |
| 'Parameter $include_app_logs must be boolean but was ' . |
| typeOrClass($include_app_logs)); |
| } |
| |
| $request->setIncludeAppLogs($include_app_logs); |
| self::setDefaultModuleVersion($request); |
| |
| if (is_string($request_ids)) { |
| if (!preg_match(self::$REQUEST_ID_REGEX, $request_ids)) { |
| throw new \InvalidArgumentException( |
| "Invalid request id " . htmlspecialchars($request_ids)); |
| } |
| $request->addRequestId($request_ids); |
| } else if (is_array($request_ids)) { |
| foreach ($request_ids as $id) { |
| if (!is_string($id)) { |
| throw new \InvalidArgumentException( |
| 'Request id must be a string but was ' . |
| self::typeOrClass($id)); |
| } |
| if (!preg_match(self::$REQUEST_ID_REGEX, $id)) { |
| throw new \InvalidArgumentException( |
| "Invalid request id " . htmlspecialchars($id)); |
| } |
| $request->addRequestId($id); |
| } |
| } else { |
| throw new \InvalidArgumentException( |
| 'Expected a string or an array of strings but was '. |
| self::typeOrClass($value)); |
| } |
| |
| $response = self::readLogs($request); |
| |
| $result = []; |
| foreach ($response->getLogList() as $log) { |
| $result[] = new RequestLog($log); |
| } |
| return $result; |
| } |
| |
| /** |
| * Translates a PHP <syslog>syslog<syslog> priority level into a Google App |
| * Engine severity level. Useful when filtering logs by minimum severity |
| * level given the syslog level. |
| * |
| * @param integer $syslog_level The priority level passed to |
| * <code>syslog</code>. |
| * @return integer The app engine severity level. |
| */ |
| public static function getAppEngineLogLevel($syslog_level) { |
| return self::$syslog_priority_map[$syslog_level]; |
| } |
| |
| private static function optionTypeException($key, $value, $expected) { |
| throw new \InvalidArgumentException( |
| htmlspecialchars("Option $key must be type $expected but was ") . |
| self::typeOrClass($value)); |
| } |
| |
| /** |
| * @return string The type or class name if type is object. |
| */ |
| private static function typeOrClass($value) { |
| if (is_object($value)) { |
| return get_class($value); |
| } else { |
| return gettype($value); |
| } |
| } |
| |
| private static function dateTimeToUsecs($datetime) { |
| // DateTime is accurate to seconds, StartTime to micro seconds. |
| // The time stamp may only represent a date up to 2038 due to 32 bit ints. |
| return (double) $datetime->getTimeStamp() * 1e6; |
| } |
| |
| /** |
| * The GAE PECL extension calls this directly instead of the built-in syslog. |
| */ |
| private static function syslog($priority, $message) { |
| $log_level = self::getAppEngineLogLevel($priority); |
| self::log($log_level, $message); |
| if (function_exists('_gae_syslog')) { |
| _gae_syslog($log_level); |
| } |
| } |
| } |
| |
| /** |
| * @internal |
| */ |
| trait ApiProxyAccess { |
| /** |
| * @param LogReadRequest $request The protocol buffer request to fetch. |
| * |
| * @return LogReadResponse The response including RequestLogs. |
| */ |
| private static function readLogs(LogReadRequest $request) { |
| $response = new LogReadResponse(); |
| try { |
| ApiProxy::makeSyncCall('logservice', 'Read', $request, $response); |
| } catch (ApplicationError $e) { |
| throw self::applicationErrorToException($e); |
| } |
| return $response; |
| } |
| |
| private static function applicationErrorToException(ApplicationError $error) { |
| switch($error->getApplicationError()) { |
| case ErrorCode::INVALID_REQUEST: |
| return new LogException('Invalid Request'); |
| case ErrorCode::STORAGE_ERROR: |
| return new LogException('Storage Error'); |
| default: |
| return new LogException( |
| 'Error Code: ' . $error->getApplicationError()); |
| } |
| } |
| } |