diff --git a/splunk/.gitignore b/splunk/.gitignore new file mode 100644 index 00000000..a7393c21 --- /dev/null +++ b/splunk/.gitignore @@ -0,0 +1,11 @@ +# OS X +.DS_Store + +# Generated documentation +apidocs/ + +# Generated code coverage report +coverage/ + +# Local settings +*.local.php diff --git a/splunk/Splunk.php b/splunk/Splunk.php new file mode 100644 index 00000000..376c37f8 --- /dev/null +++ b/splunk/Splunk.php @@ -0,0 +1,33 @@ +children(Splunk_AtomFeed::NS_S)->dict; + $listValue = $containerXml->children(Splunk_AtomFeed::NS_S)->list; + + if (Splunk_XmlUtil::elementExists($dictValue)) + { + return Splunk_AtomFeed::parseDict($dictValue); + } + else if (Splunk_XmlUtil::elementExists($listValue)) + { + return Splunk_AtomFeed::parseList($listValue); + } + else // value is scalar + { + return Splunk_XmlUtil::getTextContent($containerXml); + } + } + + /* + * Example of $dictXml: + * + * + * v1 + * v2 + * + */ + private static function parseDict($dictXml) + { + $dict = array(); + foreach ($dictXml->children(Splunk_AtomFeed::NS_S)->key as $keyXml) + { + $key = Splunk_XmlUtil::getAttributeValue($keyXml, 'name'); + $value = Splunk_AtomFeed::parseValueInside($keyXml); + + $dict[$key] = $value; + } + return $dict; + } + + /* + * Example of $listXml: + * + * + * e1 + * e2 + * + */ + private static function parseList($listXml) + { + $list = array(); + foreach ($listXml->children(Splunk_AtomFeed::NS_S)->item as $itemXml) + $list[] = Splunk_AtomFeed::parseValueInside($itemXml); + return $list; + } +} \ No newline at end of file diff --git a/splunk/Splunk/Collection.php b/splunk/Splunk/Collection.php new file mode 100644 index 00000000..82278c36 --- /dev/null +++ b/splunk/Splunk/Collection.php @@ -0,0 +1,313 @@ +entitySubclass = $entitySubclass; + } + + // === Accessors === + + /** + * Not implemented. + */ + protected function getSearchNamespace() + { + // (The namespace cannot presently be overridden on a per-collection basis.) + return NULL; + } + + // === Operations === + + // NOTE: This method isn't called 'list' only because PHP treats 'list' as a + // pseudo-keyword and gets confused when it's used as a method name. + /** + * Lists this collection's entities, returning a list of loaded entities. + * + * By default, all items in the collection are returned. For large + * collections, it is advisable to fetch items using multiple calls with + * the paging options (i.e. 'offset' and 'count'). + * + * @param array $args (optional) {
+ * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to list entities. Defaults to the service's namespace.
+ * + * **count**: (optional) The maximum number of items to return, + * or -1 to return as many as possible. + * Defaults to returning as many as possible.
+ * **offset**: (optional) The offset of the first item to return. + * Defaults to 0.
+ * + * **search**: (optional) The search expression to filter responses + * with. For example, "foo" matches any object that has + * "foo" in a substring of a field. Similarly the + * expression "field_name=field_value" matches only objects + * that have a "field_name" field with the value + * "field_value".
+ * **sort_dir**: (optional) The direction to sort returned items.
+ * Valid values:
+ * - "asc": Sort in ascending order.
+ * - "desc": Sort in descending order.
+ * Defaults to "asc".
+ * **sort_key**: (optional) The field to use for sorting. + * Defaults to "name".
+ * **sort_mode**: (optional) The sorting algorithm to use. Valid values:
+ * - "auto": If all values of the field are numbers, + * sort numerically. Otherwise, sort + * alphabetically.
+ * - "alpha": Sort alphabetically.
+ * - "alpha_case": Sort alphabetically, case-sensitive.
+ * - "num": Sort numerically.
+ * Defaults to "auto".
+ * } + * @return array the entities in the listing. + * @throws Splunk_IOException + */ + public function items($args=array()) + { + $args = array_merge(array( + 'count' => -1, + ), $args); + + if ($args['count'] <= 0 && $args['count'] != -1) + throw new InvalidArgumentException( + 'Count must be positive or -1 (infinity).'); + + if ($args['count'] == -1) + $args['count'] = 0; // infinity value for the REST API + + $response = $this->sendGet('', $args); + return $this->loadEntitiesFromResponse($response); + } + + /** + * Returns an array of entities from the given response. + * + * @param $response + * @return array array of Splunk_Entry. + */ + private function loadEntitiesFromResponse($response) + { + $xml = new SimpleXMLElement($response->body); + + $entities = array(); + foreach ($xml->entry as $entry) + { + $entities[] = $this->loadEntityFromEntry($entry); + } + return $entities; + } + + /** + * Returns an entity from the given entry element. + * + * @param SimpleXMLElement $entry an element. + * @return Splunk_Entry + */ + private function loadEntityFromEntry($entry) + { + return new $this->entitySubclass( + $this->service, + $this->getEntityPath($entry->title), + $entry); + } + + /** + * Returns the unique entity with the specified name in this collection. + * + * @param string $name The name of the entity to search for. + * @param Splunk_Namespace|NULL $namespace + * (optional) {Splunk_Namespace} The namespace in which + * to search. Defaults to the service's namespace. + * @return Splunk_Entity + * @throws Splunk_NoSuchEntityException + * If no such entity exists. + * @throws Splunk_AmbiguousEntityNameException + * If multiple entities with the specified name + * exist in the specified namespace. + * @throws Splunk_IOException + */ + public function get($name, $namespace=NULL) + { + $this->checkName($name); + $this->checkNamespace($namespace); + + try + { + $response = $this->sendGet($this->getEntityRelativePath($name), array( + 'namespace' => $namespace, + 'count' => 0, + )); + $entities = $this->loadEntitiesFromResponse($response); + } + catch (Splunk_HttpException $e) + { + if ($e->getResponse()->status == 404) + $entities = array(); + else + throw $e; + } + + if (count($entities) == 0) + { + throw new Splunk_NoSuchEntityException($name); + } + else if (count($entities) == 1) + { + return $entities[0]; + } + else + { + throw new Splunk_AmbiguousEntityNameException($name); + } + } + + /** + * Returns a reference to the unique entity with the specified name in this + * collection. Loading of the entity is deferred until its first use. + * + * @param string $name The name of the entity to search for. + * @param Splunk_Namespace|NULL $namespace + * (optional) {Splunk_Namespace} The namespace in which + * to search. Defaults to the service's namespace. + * @return Splunk_Entity + */ + public function getReference($name, $namespace=NULL) + { + $this->checkName($name); + $this->checkNamespace($namespace); + + return new $this->entitySubclass( + $this->service, + $this->getEntityPath($name), + NULL, + $namespace); + } + + /** + * Creates a new entity in this collection. + * + * @param string $name The name of the entity to create. + * @param array $args (optional) Entity-specific creation arguments, + * merged with {
+ * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to create the entity. Defaults to the service's + * namespace.
+ * } + * @return Splunk_Entity + * @throws Splunk_IOException + */ + public function create($name, $args=array()) + { + $this->checkName($name); + + $args = array_merge(array( + 'name' => $name, + ), $args); + + $response = $this->sendPost('', $args); + if ($response->body === '') + { + // This endpoint doesn't return the content of the new entity. + return $this->getReference($name); + } + else + { + $xml = new SimpleXMLElement($response->body); + return $this->loadEntityFromEntry($xml->entry); + } + } + + /** + * Deletes an entity from this collection. + * + * @param string $name The name of the entity to delete. + * @param array $args (optional) Entity-specific deletion arguments, + * merged with {
+ * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to find the entity. Defaults to the service's + * namespace.
+ * } + * @throws Splunk_IOException + */ + public function delete($name, $args=array()) + { + $this->checkName($name); + + $this->sendDelete($this->getEntityRelativePath($name), $args); + } + + // === Utility === + + /** + * Returns the absolute path of the child entity with the specified name. + */ + private function getEntityPath($name) + { + return $this->path . $this->getEntityRelativePath($name); + } + + /** + * Returns the relative path of the child entity with the specified name. + */ + private function getEntityRelativePath($name) + { + return urlencode($name); + } + + /** + * Ensures that the specified name is not NULL or empty. + */ + protected function checkName($name) + { + if ($name === NULL || $name === '') + throw new InvalidArgumentException('Invalid empty name.'); + } + + /** + * Ensures that the specified namespace is a Splunk_Namespace if it is + * not NULL. + */ + protected function checkNamespace($namespace) + { + // (It's not uncommon to attempt to pass an args dictionary after + // the $name argument, so perform an explicit type check to make sure + // the caller isn't trying to do this.) + if ($namespace !== NULL && !($namespace instanceof Splunk_Namespace)) + throw new InvalidArgumentException( + 'Namespace must be NULL or a Splunk_Namespace.'); + } +} diff --git a/splunk/Splunk/ConnectException.php b/splunk/Splunk/ConnectException.php new file mode 100644 index 00000000..1844dfe9 --- /dev/null +++ b/splunk/Splunk/ConnectException.php @@ -0,0 +1,30 @@ + + * **username**: (optional) The username to login with. Defaults to + * "admin".
+ * **password**: (optional) The password to login with. Defaults to + * "changeme".
+ * **token**: (optional) The authentication token to use. If provided, + * the username and password are ignored and there is no + * need to call login(). In the format "Splunk SESSION_KEY". + *
+ * **host**: (optional) The hostname of the Splunk server. Defaults to + * "localhost".
+ * **port**: (optional) The port of the Splunk server. Defaults to + * 8089.
+ * **scheme**: (optional) The scheme to use: either "http" or "https". + * Defaults to "https".
+ * **namespace**: (optional) Namespace that all object lookups will + * occur in by default. Defaults to + * `Splunk_Namespace::createDefault()`.
+ * **http**: (optional) An Http object that will be used for + * performing HTTP requests. This is intended for testing + * only.
+ * } + */ + public function __construct($args=array()) + { + $args = array_merge(array( + 'username' => 'admin', + 'password' => 'changeme', + 'token' => NULL, + 'host' => 'localhost', + 'port' => 8089, + 'scheme' => 'https', + 'namespace' => Splunk_Namespace::createDefault(), + 'http' => new Splunk_Http(), + ), $args); + + $this->username = $args['username']; + $this->password = $args['password']; + $this->token = $args['token']; + $this->host = $args['host']; + $this->port = $args['port']; + $this->scheme = $args['scheme']; + $this->namespace = $args['namespace']; + $this->http = $args['http']; + } + + // === Operations === + + /** + * Authenticates to the Splunk server. + */ + public function login() + { + $response = $this->http->post($this->url('/services/auth/login'), array( + 'username' => $this->username, + 'password' => $this->password, + )); + + $sessionKey = Splunk_XmlUtil::getTextContentAtXpath( + new SimpleXMLElement($response->body), + '/response/sessionKey'); + + $this->token = "Splunk {$sessionKey}"; + } + + // === HTTP === + + /** + * Sends an HTTP GET request to the endpoint at the specified path. + * + * @param string $path relative or absolute URL path. + * @param array $args (optional) query parameters, merged with {
+ * **namespace**: (optional) namespace to use, or NULL to use + * this context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::get() + */ + public function sendGet($path, $args=array()) + { + return $this->sendSimpleRequest('get', $path, $args); + } + + /** + * Sends an HTTP POST request to the endpoint at the specified path. + * + * @param string $path relative or absolute URL path. + * @param array $args (optional) form parameters to send in the + * request body, merged with {
+ * **namespace**: (optional) namespace to use, or NULL to use + * this context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::post() + */ + public function sendPost($path, $args=array()) + { + return $this->sendSimpleRequest('post', $path, $args); + } + + /** + * Sends an HTTP DELETE request to the endpoint at the specified path. + * + * @param string $path relative or absolute URL path. + * @param array $args (optional) query parameters, merged with {
+ * **namespace**: (optional) namespace to use, or NULL to use + * this context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::delete() + */ + public function sendDelete($path, $args=array()) + { + return $this->sendSimpleRequest('delete', $path, $args); + } + + /** + * Sends a simple HTTP request to the endpoint at the specified path. + */ + private function sendSimpleRequest($method, $path, $args) + { + list($params, $namespace) = + Splunk_Util::extractArgument($args, 'namespace', NULL); + + return $this->http->$method( + $this->url($path, $namespace), + $params, + $this->getRequestHeaders()); + } + + /** + * Sends an HTTP request to the endpoint at the specified path. + * + * @param string $method the HTTP method (ex: 'GET' or 'POST'). + * @param string $path relative or absolute URL path. + * @param array $requestHeaders (optional) dictionary of header names and + * values. + * @param string $requestBody (optional) content to send in the request. + * @param array $args (optional) query parameters, merged with + * {
+ * **namespace**: (optional) namespace to use, or NULL to use + * this context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::request() + */ + public function sendRequest( + $method, $path, $requestHeaders=array(), $requestBody='', $args=array()) + { + list($params, $namespace) = + Splunk_Util::extractArgument($args, 'namespace', NULL); + + $url = $this->url($path, $namespace); + $fullUrl = (count($params) == 0) + ? $url + : $url . '?' . http_build_query($params); + + $requestHeaders = array_merge( + $this->getRequestHeaders(), + $requestHeaders); + + return $this->http->request( + $method, + $fullUrl, + $requestHeaders, + $requestBody); + } + + /** Returns the standard headers to send on each HTTP request. */ + private function getRequestHeaders() + { + return array( + 'Authorization' => $this->token, + ); + } + + // === Accessors === + + /** + * Gets the default namespace for collection and entity operations. + * + * @return Splunk_Namespace The default namespace that will be used + * to perform collection and entity operations + * when none is explicitly specified. + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Gets the token used to authenticate HTTP requests after logging in. + * + * @return string The token used to authenticate HTTP requests + * after logging in. + */ + public function getToken() + { + return $this->token; + } + + /** + * Gets the hostname of the Splunk server. + * + * @return string The hostname of the Splunk server. + */ + public function getHost() + { + return $this->host; + } + + /** + * Gets the port of the Splunk server. + * + * @return string The port of the Splunk server. + */ + public function getPort() + { + return $this->port; + } + + /** + * Gets the scheme to use. + * + * @return string The scheme to use: either "http" or "https". + */ + public function getScheme() + { + return $this->scheme; + } + + // === Utility === + + /** + * Returns the absolute URL. + * + * @param string $path Relative or absolute URL path. + * @param Splunk_Namespace|NULL $namespace + * @return string Absolute URL. + */ + private function url($path, $namespace=NULL) + { + return "{$this->scheme}://{$this->host}:{$this->port}{$this->abspath($path, $namespace)}"; + } + + /** + * Returns the absolute URL path. + * + * @param string $path Relative or absolute URL path. + * @param Splunk_Namespace|NULL $namespace + * @return string Absolute URL path. + */ + private function abspath($path, $namespace=NULL) + { + if ((strlen($path) >= 1) && ($path[0] == '/')) + return $path; + if ($namespace === NULL) + $namespace = $this->namespace; + + return $namespace->getPathPrefix() . $path; + } +} diff --git a/splunk/Splunk/Endpoint.php b/splunk/Splunk/Endpoint.php new file mode 100644 index 00000000..46472f03 --- /dev/null +++ b/splunk/Splunk/Endpoint.php @@ -0,0 +1,111 @@ +service = $service; + $this->path = $path; + } + + // === Accessors === + + /** + * Returns the namespace in which this endpoint resides, or NULL to use + * the context's default namespace. + * + * @return Splunk_Namespace|NULL The namespace in which this endpoint + * resides, or NULL to use the context's + * default namespace. + * Possibly a non-exact namespace. + */ + protected abstract function getSearchNamespace(); + + // === HTTP === + + /** + * Sends an HTTP GET request relative to this endpoint. + * + * @param string $relativePath relative URL path. + * @param array $args (optional) query parameters, merged with {
+ * **namespace**: (optional) namespace to use, or NULL to use + * the context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::get() + */ + public function sendGet($relativePath, $args=array()) + { + return $this->sendSimpleRequest('sendGet', $relativePath, $args); + } + + /** + * Sends an HTTP POST request relative to this endpoint. + * + * @param string $relativePath relative URL path. + * @param array $args (optional) form parameters to send in the request + * body, merged with {
+ * **namespace**: (optional) namespace to use, or NULL to use + * the context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::post() + */ + public function sendPost($relativePath, $args=array()) + { + return $this->sendSimpleRequest('sendPost', $relativePath, $args); + } + + /** + * Sends an HTTP DELETE request relative to this endpoint. + * + * @param string $relativePath relative URL path. + * @param array $args (optional) query parameters, merged with {
+ * **namespace**: (optional) namespace to use, or NULL to use + * the context's default namespace.
+ * } + * @return Splunk_HttpResponse + * @throws Splunk_IOException + * @see Splunk_Http::delete() + */ + public function sendDelete($relativePath, $args=array()) + { + return $this->sendSimpleRequest('sendDelete', $relativePath, $args); + } + + /** Sends a simple request relative to this endpoint. */ + private function sendSimpleRequest($method, $relativePath, $args=array()) + { + $args = array_merge(array( + 'namespace' => $this->getSearchNamespace(), + ), $args); + + return $this->service->$method("{$this->path}{$relativePath}", $args); + } +} \ No newline at end of file diff --git a/splunk/Splunk/Entity.php b/splunk/Splunk/Entity.php new file mode 100644 index 00000000..87e2b6f5 --- /dev/null +++ b/splunk/Splunk/Entity.php @@ -0,0 +1,298 @@ + for this + * entity, as received from the REST API. + * If omitted, will be loaded on demand. + * @param Splunk_Namespace|NULL $namespace + * (optional) The namespace from which to + * load this entity, or NULL to use the + * $service object's default namespace. + * Does not apply if this entity is + * already loaded (i.e. if $entry is not + * NULL). + */ + public function __construct($service, $path, $entry=NULL, $namespace=NULL) + { + parent::__construct($service, $path); + + $this->entry = $entry; + if ($this->entry != NULL) + { + if ($namespace !== NULL) + throw new InvalidArgumentException( + 'Cannot specify an entry and a namespace simultaneously. ' . + 'The namespace will be inferred from the entry.'); + + $this->parseContentsFromEntry(); + $this->namespace = $this->getNamespace(); // extract from 'eai:acl' + } + else + { + $this->namespace = $namespace; + } + } + + // === Load === + + /** + * Loads this resource if not already done. Returns self. + * + * @return Splunk_Entity This entity. + * @throws Splunk_IOException + */ + protected function validate($fetchArgs=array()) + { + if (!$this->loaded) + { + $this->load($fetchArgs); + assert($this->loaded); + } + return $this; + } + + /** + * Loads this resource. + * + * @throws Splunk_IOException + */ + private function load($fetchArgs) + { + $response = $this->fetch($fetchArgs); + $xml = new SimpleXMLElement($response->body); + + $this->entry = $this->extractEntryFromRootXmlElement($xml); + $this->parseContentsFromEntry(); + } + + /** + * Fetches this entity's Atom feed from the Splunk server. + * + * @throws Splunk_IOException + */ + protected function fetch($fetchArgs) + { + return $this->sendGet(''); + } + + /** Returns the element inside the root element. */ + protected function extractEntryFromRootXmlElement($xml) + { + if (!Splunk_XmlUtil::isSingleElement($xml->entry)) + { + // Extract name from path since we can't extract it from the + // entity content here. + $pathComponents = explode('/', $this->path); + $name = $pathComponents[count($pathComponents) - 1]; + + throw new Splunk_AmbiguousEntityNameException($name); + } + + return $xml->entry; + } + + /** Parses the entry's contents. */ + private function parseContentsFromEntry() + { + $this->content = Splunk_AtomFeed::parseValueInside($this->entry->content); + $this->loaded = TRUE; + } + + /** Returns a value that indicates whether the entity has been loaded. */ + protected function isLoaded() + { + return $this->loaded; + } + + /** + * Refreshes this entity's properties from the Splunk server. + * + * @return Splunk_Entity This entity. + * @throws Splunk_IOException + */ + public function refresh() + { + if ($this->loaded) + { + // Remember this entity's exact namespace, so that a reload + // will occur in the correct namespace. + $this->namespace = $this->getNamespace(); + } + + $this->loaded = FALSE; + return $this->validate(); + } + + // === Accessors === + + /** + * Gets an array that contains the properties of this entity. + * + * @return array The properties of this entity. + */ + public function getContent() + { + return $this->validate()->content; + } + + /** + * Gets the name of this entity. + * + * @return string The name of this entity. + * This name can be used to lookup this entity + * from its collection. + */ + public function getName() + { + return $this->getTitle(); + } + + /** + * Gets the title of this entity in the REST API. + * + * @return string The title of this entity in the REST API. + */ + protected function getTitle() + { + return (string) $this->validate()->entry->title; + } + + /** Gets the namespace in which this entity resides. */ + protected function getSearchNamespace() + { + return $this->namespace; + } + + /** + * Gets the non-wildcarded namespace in which this entity resides. + * + * @return Splunk_Namespace The non-wildcarded namespace in which this + * entity resides. + */ + public function getNamespace() + { + // If this is an entity reference with an exact namespace, return it. + if (!$this->loaded) + { + $effectiveNamespace = $this->namespace; + if ($effectiveNamespace === NULL) + $effectiveNamespace = $this->service->getNamespace(); + if ($effectiveNamespace->isExact()) + return $effectiveNamespace; + } + + // Extract the namespace from this entity's content. + $acl = $this['eai:acl']; + return Splunk_Namespace::createExact( + $acl['owner'], $acl['app'], $acl['sharing']); + } + + // === ArrayAccess Methods === + + /** + * Gets the value of the specified entity property. + * + * @param string $key The name of an entity property. + * @return string The value of the specified entity property. + */ + public function offsetGet($key) + { + return $this->validate()->content[$key]; + } + + /** @internal */ + public function offsetSet($key, $value) + { + throw new Splunk_UnsupportedOperationException(); + } + + /** @internal */ + public function offsetUnset($key) + { + throw new Splunk_UnsupportedOperationException(); + } + + /** + * Gets a value that indicates whether the specified entity property exists. + * + * @param string $key The name of an entity property. + * @return string Whether the specified entity property exists. + */ + public function offsetExists($key) + { + return isset($this->validate()->content[$key]); + } + + // === Operations === + + /** + * Deletes this entity. + * + * @throws Splunk_IOException + */ + public function delete() + { + $this->sendDelete(''); + } + + /** + * Updates this entity's properties. + * + * Note that the "name" property cannot be updated. + * + * @param array $args Dictionary of properties that will be changed, + * along with their new values. + * @return Splunk_Entity This entity. + * @throws Splunk_IOException + */ + public function update($args) + { + if (array_key_exists('name', $args)) + throw new InvalidArgumentException( + 'Cannot update the name of an entity.'); + if (array_key_exists('namespace', $args)) + throw new InvalidArgumentException( + 'Cannot override the entity\'s namespace.'); + + // Update entity on server + $this->sendPost('', $args); + + // Update cached content of entity + if ($this->loaded) + $this->content = array_merge($this->content, $args); + + return $this; + } +} \ No newline at end of file diff --git a/splunk/Splunk/Http.php b/splunk/Splunk/Http.php new file mode 100644 index 00000000..f444429e --- /dev/null +++ b/splunk/Splunk/Http.php @@ -0,0 +1,232 @@ +requestWithParams('get', $url, $params, $requestHeaders); + } + + /** + * @param array $params (optional) form parameters to send in the request body. + * @see request() + */ + public function post($url, $params=array(), $requestHeaders=array()) + { + $requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + + return $this->request( + 'post', $url, $requestHeaders, http_build_query($params)); + } + + /** + * @param array $params (optional) query parameters. + * @see request() + */ + public function delete($url, $params=array(), $requestHeaders=array()) + { + return $this->requestWithParams('delete', $url, $params, $requestHeaders); + } + + private function requestWithParams( + $method, $url, $params, $requestHeaders) + { + $fullUrl = ($params === NULL || count($params) == 0) + ? $url + : $url . '?' . http_build_query($params); + + return $this->request($method, $fullUrl, $requestHeaders); + } + + /** + * Sends an HTTP request and returns the response. + * + * @param string $method HTTP request method (ex: 'get'). + * @param string $url URL to fetch. + * @param array $requestHeaders (optional) dictionary of header names and values. + * @param string $requestBody (optional) content to send in the request. + * @return Splunk_HttpResponse + * @throws Splunk_IOException + */ + public function request( + $method, $url, $requestHeaders=array(), $requestBody='') + { + $isHttp = (substr($url, 0, strlen('http:')) === 'http:'); + $isHttps = (substr($url, 0, strlen('https:')) === 'https:'); + + if (!$isHttp && !$isHttps) + { + throw new InvalidArgumentException( + 'URL scheme must be either HTTP or HTTPS.'); + } + + // The HTTP stream wrapper in PHP < 5.3.7 has a bug which + // injects junk at the end of HTTP requests, which breaks + // SSL connections. Fallback to cURL-based requests. + if ($isHttps && (version_compare(PHP_VERSION, '5.3.7') < 0)) + return $this->requestWithCurl( + $method, $url, $requestHeaders, $requestBody); + + $requestHeaderLines = array(); + foreach ($requestHeaders as $k => $v) + $requestHeaderLines[] = "{$k}: {$v}"; + + $fopenContext = stream_context_create(array( + 'http' => array( + 'method' => strtoupper($method), + 'header' => $requestHeaderLines, + 'content' => $requestBody, + 'follow_location' => 0, // don't follow HTTP 3xx automatically + 'max_redirects' => 0, // [PHP 5.2] don't follow HTTP 3xx automatically + 'ignore_errors' => TRUE, // don't throw exceptions on bad status codes + ), + // LMR FIX!!!!!! + 'ssl' => array( + 'verify_peer' => false, + 'allow_self_signed' => true, + 'verify_peer_name' => false, + ), + )); + + // NOTE: PHP does not perform certificate validation for HTTPS URLs. + // NOTE: fopen() magically sets the $http_response_header local variable. + $bodyStream = @fopen($url, 'rb', /*use_include_path=*/FALSE, $fopenContext); + if ($bodyStream === FALSE) + { + $errorInfo = error_get_last(); + $errmsg = $errorInfo['message']; + $errno = $errorInfo['type']; + throw new Splunk_ConnectException($errmsg, $errno); + } + + $headers = array(); + $headerLines = $http_response_header; + $statusLine = array_shift($headerLines); + foreach ($headerLines as $line) + { + list($key, $value) = explode(':', $line, 2); + $headers[$key] = trim($value); + } + + $statusLineComponents = explode(' ', $statusLine, 3); + $httpVersion = $statusLineComponents[0]; + $status = intval($statusLineComponents[1]); + $reason = (count($statusLineComponents) == 3) + ? $statusLineComponents[2] + : ''; + + $response = new Splunk_HttpResponse(array( + 'status' => $status, + 'reason' => $reason, + 'headers' => $headers, + 'bodyStream' => $bodyStream, + )); + + if ($status >= 400) + throw new Splunk_HttpException($response); + else + return $response; + } + + private function requestWithCurl( + $method, $url, $requestHeaders=array(), $requestBody='') + { + $opts = array( + CURLOPT_URL => $url, + CURLOPT_TIMEOUT => 60, // secs + CURLOPT_RETURNTRANSFER => TRUE, + CURLOPT_HEADER => TRUE, + // disable SSL certificate validation + CURLOPT_SSL_VERIFYPEER => FALSE, + // LMR FIX !!!!!!! + CURLOPT_SSL_VERIFYHOST => FALSE, + ); + + foreach ($requestHeaders as $k => $v) + $opts[CURLOPT_HTTPHEADER][] = "$k: $v"; + + switch ($method) + { + case 'get': + $opts[CURLOPT_HTTPGET] = TRUE; + break; + case 'post': + $opts[CURLOPT_POST] = TRUE; + $opts[CURLOPT_POSTFIELDS] = $requestBody; + break; + default: + $opts[CURLOPT_CUSTOMREQUEST] = strtoupper($method); + break; + } + + if (!($curl = curl_init())) + throw new Splunk_ConnectException('Unable to initialize cURL.'); + if (!(curl_setopt_array($curl, $opts))) + throw new Splunk_ConnectException(curl_error($curl)); + // NOTE: The entire HTTP response is read into memory here, + // which could be very large. Unfortunately the cURL + // interface does not provide a streaming alternative. + // To avoid this problem, use PHP 5.3.7+, which doesn't + // need cURL to perform HTTP requests. + if (!($response = curl_exec($curl))) + throw new Splunk_ConnectException(curl_error($curl)); + + $status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE); + $headerText = substr($response, 0, $headerSize); + $body = (strlen($response) == $headerSize) + ? '' + : substr($response, $headerSize); + + $headers = array(); + $headerLines = explode("\r\n", trim($headerText)); + $statusLine = array_shift($headerLines); + foreach ($headerLines as $line) + { + list($key, $value) = explode(':', $line, 2); + $headers[$key] = trim($value); + } + + $statusLineComponents = explode(' ', $statusLine, 3); + $httpVersion = $statusLineComponents[0]; + $reason = count($statusLineComponents) == 3 ? $statusLineComponents[2] : ''; + + $response = new Splunk_HttpResponse(array( + 'status' => $status, + 'reason' => $reason, + 'headers' => $headers, + 'body' => $body, + )); + + if ($status >= 400) + throw new Splunk_HttpException($response); + else + return $response; + } +} diff --git a/splunk/Splunk/HttpException.php b/splunk/Splunk/HttpException.php new file mode 100644 index 00000000..afcbd750 --- /dev/null +++ b/splunk/Splunk/HttpException.php @@ -0,0 +1,64 @@ +status} {$response->reason}"; + if ($detail != NULL) + $message .= ' -- ' . $detail; + + $this->response = $response; + parent::__construct($message); + } + + /** Parses an HTTP response. */ + private static function parseFirstMessageFrom($response) + { + if ($response->body == '') + return NULL; + + return Splunk_XmlUtil::getTextContentAtXpath( + new SimpleXMLElement($response->body), + '/response/messages/msg'); + } + + // === Accessors === + + /** + * Gets an HTTP response. + * + * @return Splunk_HttpResponse + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/splunk/Splunk/HttpResponse.php b/splunk/Splunk/HttpResponse.php new file mode 100644 index 00000000..ff6b9100 --- /dev/null +++ b/splunk/Splunk/HttpResponse.php @@ -0,0 +1,97 @@ + '0')). + * @property-read string $body Content of the response, + * as a single byte string. + * @property-read resource $bodyStream + * Content of the response, as a stream + * (of the type returned by fopen()). + */ +class Splunk_HttpResponse +{ + private $state; + private $body; // lazy + private $bodyStream; // lazy + + /* @internal */ + public function __construct($state) + { + $this->state = $state; + $this->body = NULL; + $this->bodyStream = NULL; + } + + // === Accessors === + + /** @internal */ + public function __get($key) + { + if ($key === 'body') + return $this->getBody(); + else if ($key === 'bodyStream') + return $this->getBodyStream(); + else + return $this->state[$key]; + } + + private function getBody() + { + if (array_key_exists('body', $this->state)) + return $this->state['body']; + + if ($this->body === NULL) + { + if (!array_key_exists('bodyStream', $this->state)) + throw new Splunk_UnsupportedOperationException( + 'Response object does not contain body stream.'); + + $this->body = Splunk_Util::stream_get_contents( + $this->state['bodyStream']); + } + return $this->body; + } + + private function getBodyStream() + { + if (array_key_exists('bodyStream', $this->state)) + return $this->state['bodyStream']; + + if ($this->bodyStream === NULL) + { + if (!array_key_exists('body', $this->state)) + throw new Splunk_UnsupportedOperationException( + 'Response object does not contain body.'); + + $this->bodyStream = Splunk_StringStream::create($this->state['body']); + } + return $this->bodyStream; + } +} diff --git a/splunk/Splunk/IOException.php b/splunk/Splunk/IOException.php new file mode 100644 index 00000000..6f611826 --- /dev/null +++ b/splunk/Splunk/IOException.php @@ -0,0 +1,23 @@ + + * **host**: (optional) The value to populate in the host field + * for events from this data input.
+ * **host_regex**: (optional) A regular expression used to + * extract the host value from each event.
+ * **source**: (optional) The source value to fill in the + * metadata for this input's events.
+ * **sourcetype**: (optional) The sourcetype to apply to + * events from this input.
+ * } + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTinput#receivers.2Fsimple + */ + public function submit($data, $args=array()) + { + $this->service->getReceiver()->submit($data, array_merge($args, array( + 'index' => $this->getName(), + ))); + } + + /** + * Creates a stream for logging events to the specified index. + * + * It is highly recommended to specify a sourcetype explicitly. + * + * It is slightly faster to use {@link Splunk_Receiver::attach()} + * to accomplish the same task. One fewer network request is needed. + * + * The returned stream should eventually be closed via fclose(). + * + * @param array $args (optional) {
+ * **host**: (optional) The value to populate in the host field + * for events from this data input.
+ * **host_regex**: (optional) A regular expression used to + * extract the host value from each event.
+ * **source**: (optional) The source value to fill in the + * metadata for this input's events.
+ * **sourcetype**: (optional) The sourcetype to apply to + * events from this input.
+ * } + * @return resource A stream that you can write event text to. + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTinput#receivers.2Fstream + */ + public function attach($args=array()) + { + return $this->service->getReceiver()->attach(array_merge($args, array( + 'index' => $this->getName(), + ))); + } +} diff --git a/splunk/Splunk/Job.php b/splunk/Splunk/Job.php new file mode 100644 index 00000000..9ea8a11d --- /dev/null +++ b/splunk/Splunk/Job.php @@ -0,0 +1,413 @@ + Splunk_Job::DEFAULT_FETCH_MAX_TRIES, + 'delayPerRetry' => Splunk_Job::DEFAULT_FETCH_DELAY_PER_RETRY, + ), $fetchArgs); + + for ($numTries = 0; $numTries < $fetchArgs['maxTries']; $numTries++) + { + $response = parent::fetch($fetchArgs); + if ($this->isFullResponse($response)) + return $response; + usleep($fetchArgs['delayPerRetry'] * 1000000); + } + + // Give up + throw new Splunk_HttpException($response); + } + + protected function extractEntryFromRootXmlElement($xml) + { + // element is at the root of a job's Atom feed + return $xml; + } + + // === Ready === + + /** + * Returns a value that indicates whether this job has been loaded. + * + * @return bool Whether this job has been loaded. + */ + public function isReady() + { + return $this->isLoaded(); + } + + /** + * Loads this job, retrying the specified number of times as necessary. + * + * @param int $maxTries The maximum number of times to try loading + * this job. + * @param float $delayPerRetry The number of seconds to wait between + * attempts to retry loading this job. + * @return Splunk_Entity This entity. + * @throws Splunk_IOException + */ + public function makeReady( + $maxTries=Splunk_Job::DEFAULT_FETCH_MAX_TRIES, + $delayPerRetry=Splunk_Job::DEFAULT_FETCH_DELAY_PER_RETRY) + { + return $this->validate(/*fetchArgs=*/array( + 'maxTries' => $maxTries, + 'delayPerRetry' => $delayPerRetry, + )); + } + + // === Accessors === + + // Overrides superclass to return the correct ID of this job, + // which can be used to lookup this job from the Jobs collection. + /** + * @see Splunk_Entity::getName() + */ + public function getName() + { + return $this['sid']; + } + + /** + * Returns the search string executed by this job. + * + * @return string The search string executed by this job. + */ + public function getSearch() + { + return $this->getTitle(); + } + + // === Results === + + /** + * Returns a value that indicates the percentage of this job's results + * that were computed at the time this job was last loaded or + * refreshed. + * + * @return float Percentage of this job's results that were + * computed (0.0-1.0) at the time this job was + * last loaded or refreshed. + * @see Splunk_Entity::refresh() + */ + public function getProgress() + { + return floatval($this['doneProgress']); + } + + /** + * Returns a value that indicates whether this job's results were available + * at the time this job was last loaded or refreshed. + * + * @return boolean Whether this job's results were available + * at the time this job was last loaded or + * refreshed. + * @see Splunk_Entity::refresh() + */ + public function isDone() + { + return ($this['isDone'] === '1'); + } + + /** + * Returns an iterator over the results from this job. + * + * Large result sets will be paginated automatically. + * + * Example: + * + *
+     *  $job = ...;
+     *  while (!$job->refresh()->isDone()) { usleep(0.5 * 1000000); }
+     *  
+     *  foreach ($job->getResults() as $result)
+     *  {
+     *      // (See documentation for Splunk_ResultsReader to see how to
+     *      //  interpret $result.)
+     *      ...
+     *  }
+     * 
+ * + * This method cannot be used to access results from realtime jobs, + * which are never done. Use {@link getResultsPreviewPage()} instead. + * + * @param array $args (optional) {
+ * **count**: (optional) The maximum number of results to return, + * or -1 to return as many as possible. + * Defaults to returning as many as possible.
+ * **offset**: (optional) The offset of the first result to return. + * Defaults to 0.
+ * **pagesize**: (optional) The number of results to fetch from the + * server on each request when paginating internally, + * or -1 to return as many results as possible. + * Defaults to returning as many results as possible.
+ * + * **field_list**: (optional) Comma-separated list of fields to return + * in the result set. Defaults to all fields.
+ * **output_mode**: (optional) The output format of the result. Valid + * values:
+ * - "csv"
+ * - "raw"
+ * - "xml": The format parsed by Splunk_ResultsReader. + *
+ * - "json"
+ * Defaults to "xml".
+ * You should not change this unless you are parsing + * results yourself.
+ * **search**: (optional) The post processing search to apply to + * results. Can be any valid search language string. + * For example "search sourcetype=splunkd" will match any + * result whose "sourcetype" field is "splunkd".
+ * } + * @return Iterator The results (i.e. transformed events) + * of this job, via an iterator. + * @throws Splunk_JobNotDoneException + * If the results are not ready yet. + * Check isDone() to ensure the results are + * ready prior to calling this method. + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D.2Fresults + */ + public function getResults($args=array()) + { + return new Splunk_PaginatedResultsReader($this, $args); + } + + /** + * Returns a single page of results from this job. + * + * Most potential callers should use {@link getResults()} instead. + * Only use this method if you wish to parse job results yourself + * or want to control pagination manually. + * + * By default, all results are returned. For large + * result sets, it is advisable to fetch items using multiple calls with + * the paging options (i.e. 'offset' and 'count'). + * + * The format of the results depends on the 'output_mode' argument + * (which defaults to "xml"). XML-formatted results can be parsed + * using {@link Splunk_ResultsReader}. For example: + * + *
+     *  $job = ...;
+     *  while (!$job->refresh()->isDone()) { usleep(0.5 * 1000000); }
+     *  
+     *  $results = new Splunk_ResultsReader($job->getResultsPage());
+     *  foreach ($results as $result)
+     *  {
+     *      // (See documentation for Splunk_ResultsReader to see how to
+     *      //  interpret $result.)
+     *      ...
+     *  }
+     * 
+ * + * This method cannot be used to access results from realtime jobs, + * which are never done. Use {@link getResultsPreviewPage()} instead. + * + * @param array $args (optional) {
+ * **count**: (optional) The maximum number of results to return, + * or -1 to return as many as possible. + * Defaults to returning as many as possible.
+ * **offset**: (optional) The offset of the first result to return. + * Defaults to 0.
+ * + * **field_list**: (optional) Comma-separated list of fields to return + * in the result set. Defaults to all fields.
+ * **output_mode**: (optional) The output format of the result. Valid + * values:
+ * - "csv"
+ * - "raw"
+ * - "xml": The format parsed by Splunk_ResultsReader. + *
+ * - "json"
+ * Defaults to "xml".
+ * You should not change this unless you are parsing + * results yourself.
+ * **search**: (optional) The post processing search to apply to + * results. Can be any valid search language string. + * For example "search sourcetype=splunkd" will match any + * result whose "sourcetype" field is "splunkd".
+ * } + * @return resource The results (i.e. transformed events) + * of this job, as a stream. + * @throws Splunk_JobNotDoneException + * If the results are not ready yet. + * Check isDone() to ensure the results are + * ready prior to calling this method. + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D.2Fresults + */ + public function getResultsPage($args=array()) + { + $response = $this->fetchPage('results', $args); + if ($response->status == 204) + throw new Splunk_JobNotDoneException($response); + return $response->bodyStream; + } + + /** + * Returns a single page of results from this job, + * which may or may not be done running. + * + * @param array $args (optional) {
+ * **count**: (optional) The maximum number of results to return, + * or -1 to return as many as possible. + * Defaults to returning as many as possible.
+ * **offset**: (optional) The offset of the first result to return. + * Defaults to 0.
+ * + * **field_list**: (optional) Comma-separated list of fields to return + * in the result set. Defaults to all fields.
+ * **output_mode**: (optional) The output format of the result. Valid + * values:
+ * - "csv"
+ * - "raw"
+ * - "xml": The format parsed by Splunk_ResultsReader. + *
+ * - "json"
+ * Defaults to "xml".
+ * You should not change this unless you are parsing + * results yourself.
+ * **search**: (optional) The post processing search to apply to + * results. Can be any valid search language string. + * For example "search sourcetype=splunkd" will match any + * result whose "sourcetype" field is "splunkd".
+ * } + * @return resource The results (i.e. transformed events) + * of this job, as a stream. + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTsearch#search.2Fjobs.2F.7Bsearch_id.7D.2Fresults_preview + */ + public function getResultsPreviewPage($args=array()) + { + $response = $this->fetchPage('results_preview', $args); + if ($response->status == 204) + { + // The REST API throws a 204 when a preview is being generated + // and no results are available. This isn't a friendly behavior + // for clients. + return Splunk_StringStream::create(''); + } + return $response->bodyStream; + } + + /** Fetches a page of the specified type. */ + private function fetchPage($pageType, $args) + { + $args = array_merge(array( + 'count' => -1, + ), $args); + + if ($args['count'] <= 0 && $args['count'] != -1) + throw new InvalidArgumentException( + 'Count must be positive or -1 (infinity).'); + + if ($args['count'] == -1) + $args['count'] = 0; // infinity value for the REST API + + $response = $this->sendGet("/{$pageType}", $args); + return $response; + } + + /** Determines whether a response contains full or partial results */ + private function isFullResponse($response) + { + if ($response->status == 204) + $result = FALSE; + else + { + $responseBody = new SimpleXMLElement($response->body); + $dispatchState = implode($responseBody->content->xpath('s:dict/s:key[@name="dispatchState"]/text()')); + $result = !($dispatchState === 'QUEUED' || $dispatchState === 'PARSING'); + } + return $result; + } + + // === Control === + + /** + * Pauses this search job. + * + * @throws Splunk_IOException + */ + public function pause() + { + $this->sendControlAction('pause'); + } + + /** + * Unpauses this search job. + * + * @throws Splunk_IOException + */ + public function unpause() + { + $this->sendControlAction('unpause'); + } + + /** + * Stops this search job but keeps the partial results. + * + * @throws Splunk_IOException + */ + public function finalize() + { + $this->sendControlAction('finalize'); + } + + /** + * Stops this search job and deletes the results. + * + * @throws Splunk_IOException + */ + public function cancel() + { + $this->sendControlAction('cancel'); + } + + /** + * Posts the specified control action. + * + * @throws Splunk_IOException + */ + private function sendControlAction($actionName) + { + $response = $this->sendPost('/control', array( + 'action' => $actionName, + )); + } +} diff --git a/splunk/Splunk/JobNotDoneException.php b/splunk/Splunk/JobNotDoneException.php new file mode 100644 index 00000000..738faea5 --- /dev/null +++ b/splunk/Splunk/JobNotDoneException.php @@ -0,0 +1,26 @@ +checkName($name); + $this->checkNamespace($namespace); + + // Delegate to Job, which already has the special handling to + // fetch an individual Job entity. + return $this->getReference($name, $namespace)->makeReady(); + } + + /** + * Creates a new search job. + * + * @param string $search The search query for the job to perform. + * @param array $args (optional) Job-specific creation arguments, + * merged with {
+ * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to create the entity. Defaults to the service's + * namespace.
+ * }
+ * For details, see the + * + * "POST search/jobs" + * endpoint in the REST API Documentation. + * @return Splunk_Job + * @throws Splunk_IOException + */ + public function create($search, $args=array()) + { + $args = array_merge(array( + 'search' => $search, + ), $args); + + if (array_key_exists('exec_mode', $args) && ($args['exec_mode'] === 'oneshot')) + throw new InvalidArgumentException( + 'Cannot create oneshot jobs with this method. Use createOneshot() instead.'); + + $namespace = Splunk_Util::getArgument($args, 'namespace', NULL); + + $response = $this->sendPost('', $args); + $xml = new SimpleXMLElement($response->body); + $sid = Splunk_XmlUtil::getTextContentAtXpath($xml, '/response/sid'); + return $this->getReference($sid, $namespace); + } + + /** + * Executes the specified search query and returns results immediately. + * + * @param string $search The search query for the job to perform. + * @param array $args (optional) Job-specific creation arguments, + * merged with {
+ * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to create the entity. Defaults to the service's + * namespace.
+ * }
+ * For details, see the + * + * "POST search/jobs" + * endpoint in the REST API Documentation. + * @return string The search results, which can be parsed with + * Splunk_ResultsReader. + * @throws Splunk_IOException + */ + public function createOneshot($search, $args=array()) + { + $args = array_merge(array( + 'search' => $search, + 'exec_mode' => 'oneshot', + ), $args); + + if ($args['exec_mode'] !== 'oneshot') + throw new InvalidArgumentException( + 'Cannot override "exec_mode" with value other than "oneshot".'); + + $response = $this->sendPost('', $args); + return $response->body; + } +} diff --git a/splunk/Splunk/Namespace.php b/splunk/Splunk/Namespace.php new file mode 100644 index 00000000..42ac977f --- /dev/null +++ b/splunk/Splunk/Namespace.php @@ -0,0 +1,254 @@ +owner = $owner; + $this->app = $app; + $this->sharing = $sharing; + } + + /** + * Creates the default namespace. + * + * Objects in the default namespace correspond to the authenticated user + * and their default Splunk application. + * + * @return Splunk_Namespace + */ + public static function createDefault() + { + $numArgs = func_num_args(); // must be own line for PHP < 5.3.0 + Splunk_Namespace::ensureArgumentCountEquals(0, $numArgs); + + static $defaultNamespace = NULL; + if ($defaultNamespace === NULL) + $defaultNamespace = new Splunk_Namespace(NULL, NULL, 'default'); + return $defaultNamespace; + } + + /** + * Creates the namespace containing objects associated with the specified + * user and application. + * + * @param string|NULL $owner name of a Splunk user (ex: "admin"), + * or NULL to specify all users. + * @param string|NULL $app name of a Splunk app (ex: "search"), + * or NULL to specify all apps. + * @return Splunk_Namespace + */ + public static function createUser($owner, $app) + { + $numArgs = func_num_args(); // must be own line for PHP < 5.3.0 + Splunk_Namespace::ensureArgumentCountEquals(2, $numArgs); + + if ($owner === '' || $owner === 'nobody' || $owner === '-') + throw new InvalidArgumentException('Invalid owner.'); + if ($app === '' || $app === 'system' || $app === '-') + throw new InvalidArgumentException('Invalid app.'); + if ($owner === NULL) + $owner = '-'; + if ($app === NULL) + $app = '-'; + return new Splunk_Namespace($owner, $app, 'user'); + } + + /** + * Creates the non-global namespace containing objects associated with the + * specified application. + * + * @param string|NULL $app name of a Splunk app (ex: "search"), + * or NULL to specify all apps. + * @return Splunk_Namespace + */ + public static function createApp($app) + { + $numArgs = func_num_args(); // must be own line for PHP < 5.3.0 + Splunk_Namespace::ensureArgumentCountEquals(1, $numArgs); + + if ($app === '' || $app === 'system' || $app === '-') + throw new InvalidArgumentException('Invalid app.'); + if ($app === NULL) + $app = '-'; + return new Splunk_Namespace('nobody', $app, 'app'); + } + + /** + * Creates the global namespace containing objects associated with the + * specified application. + * + * @param string|NULL $app name of a Splunk app (ex: "search"), + * or NULL to specify all apps. + * @return Splunk_Namespace + */ + public static function createGlobal($app) + { + $numArgs = func_num_args(); // must be own line for PHP < 5.3.0 + Splunk_Namespace::ensureArgumentCountEquals(1, $numArgs); + + if ($app === '' || $app === 'system' || $app === '-') + throw new InvalidArgumentException('Invalid app.'); + if ($app === NULL) + $app = '-'; + return new Splunk_Namespace('nobody', $app, 'global'); + } + + /** + * Creates the system namespace. + * + * Objects in the system namespace ship with Splunk. + * + * @return Splunk_Namespace + */ + public static function createSystem() + { + $numArgs = func_num_args(); // must be own line for PHP < 5.3.0 + Splunk_Namespace::ensureArgumentCountEquals(0, $numArgs); + + static $system = NULL; + if ($system === NULL) + $system = new Splunk_Namespace('nobody', 'system', 'system'); + return $system; + } + + /** + * Creates a non-wildcarded namespace with the specified properties. + * + * @param string $owner name of a Splunk user (ex: "admin"). + * @param string $app name of a Splunk app (ex: "search"). + * @param string $sharing one of {'user', 'app', 'global', 'system'}. + * @see user() + */ + public static function createExact($owner, $app, $sharing) + { + $numArgs = func_num_args(); // must be own line for PHP < 5.3.0 + Splunk_Namespace::ensureArgumentCountEquals(3, $numArgs); + + if (!in_array($sharing, array('user', 'app', 'global', 'system'))) + throw new InvalidArgumentException('Invalid sharing.'); + if ($owner === NULL || $owner === '' || $owner === '-') + throw new InvalidArgumentException('Invalid owner.'); + if ($app === NULL || $app === '' || $app === '-') + throw new InvalidArgumentException('Invalid app.'); + + return new Splunk_Namespace($owner, $app, $sharing); + } + + // === Accessors === + + /** Returns the path prefix to use when referencing objects in this + namespace. */ + public function getPathPrefix() + { + switch ($this->sharing) + { + case 'default': + return '/services/'; + case 'user': + case 'app': + case 'global': + case 'system': + return '/servicesNS/' . urlencode($this->owner) . '/' . urlencode($this->app) . '/'; + default: + throw new Exception("Invalid sharing mode '{$this->sharing}'."); + } + } + + /** + * Returns whether this is an exact (non-wildcarded) namespace. + * + * Within an exact namespace, no two objects can have the same name. + */ + public function isExact() + { + return ($this->owner !== '-') && ($this->app !== '-'); + } + + /** + * Returns the user who owns objects in this namespace. + * + * This operation is only defined for exact namespaces. + */ + public function getOwner() + { + $this->ensureExact(); + return $this->owner; + } + + /** + * Returns the app associated with objects in this namespace. + * + * This operation is only defined for exact namespaces. + */ + public function getApp() + { + $this->ensureExact(); + return $this->app; + } + + /** + * Returns the sharing mode of this namespace. + * + * This operation is only defined for exact namespaces. + */ + public function getSharing() + { + $this->ensureExact(); + return $this->sharing; + } + + // === Utility === + + // (Explicitly check the argument count because many creation function + // names do not make the required number of arguments clear and PHP + // does not check under certain circumstances.) + /** Throws an exception if the number of arguments is not what was + expected. */ + private static function ensureArgumentCountEquals($expected, $actual) + { + if ($actual !== $expected) + throw new InvalidArgumentException( + "Expected exactly ${expected} arguments."); + } + + /** Throws an exception if this namespace is not an exact (non-wildcarded) + namespace. */ + private function ensureExact() + { + if (!$this->isExact()) + throw new Splunk_UnsupportedOperationException( + 'This operation is supported only for exact namespaces.'); + } +} \ No newline at end of file diff --git a/splunk/Splunk/NoSuchEntityException.php b/splunk/Splunk/NoSuchEntityException.php new file mode 100644 index 00000000..7bc62b5e --- /dev/null +++ b/splunk/Splunk/NoSuchEntityException.php @@ -0,0 +1,30 @@ += 0.'); + if ($count <= 0 && $count != -1) + throw new InvalidArgumentException( + 'Count must be positive or -1 (infinity).'); + + // (Use PHP_INT_MAX for infinity internally because it works + // well with the min() function.) + if ($pageMaxSize == -1) + $pageMaxSize = PHP_INT_MAX; // internal infinity value + if ($count == -1) + $count = PHP_INT_MAX; // internal infinity value + + $this->job = $job; + $this->args = $args; + + $this->curPageResults = NULL; + $this->curOffset = $offset; + $this->limOffset = ($count == PHP_INT_MAX) ? PHP_INT_MAX : ($offset + $count); + $this->pageMaxSize = $pageMaxSize; + $this->fieldOrderWasReturned = FALSE; + + $this->currentElement = $this->readNextElement(); + $this->atStart = TRUE; + } + + // === Iterator Methods === + + public function rewind() + { + if ($this->atStart) + return; + + throw new Splunk_UnsupportedOperationException( + 'Cannot rewind after reading past the first element.'); + } + + public function valid() + { + return ($this->currentElement !== NULL); + } + + public function next() + { + $this->currentElement = $this->readNextElement(); + $this->atStart = FALSE; + } + + public function current() + { + return $this->currentElement; + } + + public function key() + { + return NULL; + } + + // === Read Next Element === + + private function readNextElement() + { + if ($this->curPageResultsIterator == NULL || + !$this->curPageResultsIterator->valid()) + { + if ($this->curOffset >= $this->limOffset) + { + return NULL; // at EOF + } + + $numRemaining = ($this->limOffset == PHP_INT_MAX) + ? PHP_INT_MAX + : ($this->limOffset - $this->curOffset); + + $curPageMaxSize = min($this->pageMaxSize, $numRemaining); + if ($curPageMaxSize == PHP_INT_MAX) + $curPageMaxSize = -1; // infinity value for getResultsPage() + + $this->curPageResultsIterator = new Splunk_ResultsReader( + $this->job->getResultsPage( + array_merge($this->args, array( + 'offset' => $this->curOffset, + 'count' => $curPageMaxSize, + 'output_mode' => 'xml', + )))); + if (!$this->curPageResultsIterator->valid()) + { + $this->limOffset = $this->curOffset; // remember EOF position + return NULL; // at EOF + } + } + + assert($this->curPageResultsIterator->valid()); + $element = $this->curPageResultsIterator->current(); + $this->curPageResultsIterator->next(); + + if ($element instanceof Splunk_ResultsFieldOrder) + { + // Only return the field order once. + if ($this->fieldOrderWasReturned) + { + // Don't return the field order again. + // Skip to the next element. + return $this->readNextElement(); + } + else + { + $this->fieldOrderWasReturned = TRUE; + } + } + else if (is_array($element)) + { + $this->curOffset++; + } + + return $element; + } +} \ No newline at end of file diff --git a/splunk/Splunk/Receiver.php b/splunk/Splunk/Receiver.php new file mode 100644 index 00000000..e351d0b7 --- /dev/null +++ b/splunk/Splunk/Receiver.php @@ -0,0 +1,127 @@ +service = $service; + } + + // === Operations === + + /** + * Logs one or more events to the specified index. + * + * In addition to the index name it is highly recommended to specify + * a sourcetype explicitly. + * + * @param string $data Raw event text. + * This may contain data for multiple events. + * Under the default configuration, line breaks + * ("\n") can be inserted to separate multiple events. + * @param array $args (optional) {
+ * **host**: (optional) The value to populate in the host field + * for events from this data input.
+ * **host_regex**: (optional) A regular expression used to + * extract the host value from each event.
+ * **index**: (optional) The index to send events from this + * input to. Highly recommended. Defaults to "default".
+ * **source**: (optional) The source value to fill in the + * metadata for this input's events.
+ * **sourcetype**: (optional) The sourcetype to apply to + * events from this input.
+ * } + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTinput#receivers.2Fsimple + */ + public function submit($data, $args=array()) + { + // (Avoid the normal post() method, since we aren't sending form data.) + $this->service->sendRequest( + 'post', '/services/receivers/simple', + array('Content-Type' => 'text/plain'), + $data, + $args); + } + + /** + * Creates a stream for logging events to the specified index. + * + * In addition to the index name it is highly recommended to specify + * a sourcetype explicitly. + * + * The returned stream should eventually be closed via fclose(). + * + * @param array $args (optional) {
+ * **host**: (optional) The value to populate in the host field + * for events from this data input.
+ * **host_regex**: (optional) A regular expression used to + * extract the host value from each event.
+ * **index**: (optional) The index to send events from this + * input to. Highly recommended. Defaults to "default".
+ * **source**: (optional) The source value to fill in the + * metadata for this input's events.
+ * **sourcetype**: (optional) The sourcetype to apply to + * events from this input.
+ * } + * @return resource A stream that you can write event text to. + * @throws Splunk_IOException + * @link http://docs.splunk.com/Documentation/Splunk/latest/RESTAPI/RESTinput#receivers.2Fstream + */ + public function attach($args=array()) + { + $scheme = $this->service->getScheme(); + $host = $this->service->getHost(); + $port = $this->service->getPort(); + + $errno = 0; + $errstr = ''; + if ($scheme == 'http') + $stream = @fsockopen($host, $port, /*out*/ $errno, /*out*/ $errstr); + else if ($scheme == 'https') + $stream = @fsockopen('ssl://' . $host, $port, /*out*/ $errno, /*out*/ $errstr); + else + throw new Splunk_UnsupportedOperationException( + 'Unsupported URL scheme.'); + if ($stream === FALSE) + throw new Splunk_ConnectException($errstr, $errno); + + $path = '/services/receivers/stream?' . http_build_query($args); + $token = $this->service->getToken(); + + $headers = array( + "POST {$path} HTTP/1.1\r\n", + "Host: {$host}:{$port}\r\n", + "Accept-Encoding: identity\r\n", + "Authorization: {$token}\r\n", + "X-Splunk-Input-Mode: Streaming\r\n", + "\r\n", + ); + Splunk_Util::fwriteall($stream, implode('', $headers)); + + return $stream; + } +} \ No newline at end of file diff --git a/splunk/Splunk/ResultsFieldOrder.php b/splunk/Splunk/ResultsFieldOrder.php new file mode 100644 index 00000000..3c131d46 --- /dev/null +++ b/splunk/Splunk/ResultsFieldOrder.php @@ -0,0 +1,45 @@ +fieldNames = $fieldNames; + } + + /** + * Gets an ordered list of field names that will be returned in the results stream. + * + * @return array A ordered list of the field names that will be returned + * in the results stream. + */ + public function getFieldNames() + { + return $this->fieldNames; + } +} diff --git a/splunk/Splunk/ResultsMessage.php b/splunk/Splunk/ResultsMessage.php new file mode 100644 index 00000000..d764b709 --- /dev/null +++ b/splunk/Splunk/ResultsMessage.php @@ -0,0 +1,55 @@ +type = $type; + $this->text = $text; + } + + /** + * Gets the type of this message. + * + * @return string The type of this message (ex: 'DEBUG'). + */ + public function getType() + { + return $this->type; + } + + /** + * Gets the text of this message. + * + * @return string The text of this message. + */ + public function getText() + { + return $this->text; + } +} \ No newline at end of file diff --git a/splunk/Splunk/ResultsReader.php b/splunk/Splunk/ResultsReader.php new file mode 100644 index 00000000..4df09a0c --- /dev/null +++ b/splunk/Splunk/ResultsReader.php @@ -0,0 +1,314 @@ + + * $resultsReader = new Splunk_ResultsReader(...); + * foreach ($resultsReader as $result) + * { + * if ($result instanceof Splunk_ResultsFieldOrder) + * { + * // Process the field order + * print "FIELDS: " . implode(',', $result->getFieldNames()) . "\r\n"; + * } + * else if ($result instanceof Splunk_ResultsMessage) + * { + * // Process a message + * print "[{$result->getType()}] {$result->getText()}\r\n"; + * } + * else if (is_array($result)) + * { + * // Process a row + * print "{\r\n"; + * foreach ($result as $key => $valueOrValues) + * { + * if (is_array($valueOrValues)) + * { + * $values = $valueOrValues; + * $valuesString = implode(',', $values); + * print " {$key} => [{$valuesString}]\r\n"; + * } + * else + * { + * $value = $valueOrValues; + * print " {$key} => {$value}\r\n"; + * } + * } + * print "}\r\n"; + * } + * else + * { + * // Ignore unknown result type + * } + * } + * + * + * @package Splunk + */ +class Splunk_ResultsReader implements Iterator +{ + private $emptyXml; + private $xmlReader; + + private $currentElement; + private $atStart; + + /** + * Constructs a new search results string or stream. + * + * @param string|resource $streamOrXmlString + * A string or stream containing results obtained from the + * {@link Splunk_Job::getResultsPage()} method. + */ + public function __construct($streamOrXmlString) + { + if (is_string($streamOrXmlString)) + { + $string = $streamOrXmlString; + $stream = Splunk_StringStream::create($string); + } + else + { + $stream = $streamOrXmlString; + } + + // Search jobs lacking results return a blank document (with HTTP 200). + if (feof($stream)) + { + $this->emptyXml = TRUE; + + $this->currentElement = NULL; + $this->atStart = TRUE; + return; + } + else + { + $this->emptyXml = FALSE; + } + + $streamUri = Splunk_StreamStream::createUriForStream($stream); + + $this->xmlReader = new XMLReader(); + $this->xmlReader->open($streamUri); + + $this->currentElement = $this->readNextElement(); + $this->atStart = TRUE; + } + + // === Iterator Methods === + + /** @internal */ + public function rewind() + { + if ($this->atStart) + return; + + throw new Splunk_UnsupportedOperationException( + 'Cannot rewind after reading past the first element.'); + } + + /** + * Returns a value that indicates whether there are any more elements in the stream. + * + * @return boolean Whether there are any more elements. + */ + public function valid() + { + return ($this->currentElement !== NULL); + } + + /** + * Advances this iterator to the next element. + */ + public function next() + { + $this->currentElement = $this->readNextElement(); + $this->atStart = FALSE; + } + + /** + * Returns the current element of this iterator. + * + * @return Splunk_ResultsFieldOrder|Splunk_ResultsMessage|array|mixed + * The current element of this iterator. + */ + public function current() + { + return $this->currentElement; + } + + /** @internal */ + public function key() + { + return NULL; + } + + // === Read Next Element === + + /** Returns the next element in the stream. */ + private function readNextElement() + { + $xr = $this->xmlReader; + + if ($this->emptyXml) + return NULL; + + while ($xr->read()) + { + // Read: /meta + if ($xr->nodeType == XMLReader::ELEMENT && + $xr->name === 'meta') + { + return $this->readMeta(); + } + + // Read: /messages/msg + if ($xr->nodeType == XMLReader::ELEMENT && + $xr->name === 'msg') + { + $type = $xr->getAttribute('type'); + + // Read: /messages/msg/[TEXT] + if (!$xr->read()) + break; + assert ($xr->nodeType == XMLReader::TEXT); + $text = $xr->value; + + return new Splunk_ResultsMessage($type, $text); + } + + // Read: /result + if ($xr->nodeType == XMLReader::ELEMENT && + $xr->name === 'result') + { + return $this->readResult(); + } + } + return NULL; + } + + /** Reads metadata from the stream. */ + private function readMeta() + { + $xr = $this->xmlReader; + + $insideFieldOrder = FALSE; + $fieldsNames = NULL; + + while ($xr->read()) + { + // Begin: /meta/fieldOrder + if ($xr->nodeType == XMLReader::ELEMENT && + $xr->name === 'fieldOrder') + { + $insideFieldOrder = TRUE; + $fieldsNames = array(); + } + + // Read: /meta/fieldOrder/field/[TEXT] + if ($insideFieldOrder && + $xr->nodeType == XMLReader::TEXT) + { + $fieldsNames[] = $xr->value; + } + + // End: /meta/fieldOrder + if ($xr->nodeType == XMLReader::END_ELEMENT && + $xr->name === 'fieldOrder') + { + return new Splunk_ResultsFieldOrder($fieldsNames); + } + } + + throw new Exception('Syntax error in element.'); + } + + /** Returns search results from the stream. */ + private function readResult() + { + $xr = $this->xmlReader; + + $lastKey = NULL; + $lastValues = array(); + $insideValue = FALSE; + + $result = array(); + while ($xr->read()) + { + // Begin: /result/field + if ($xr->nodeType == XMLReader::ELEMENT && + $xr->name === 'field') + { + $lastKey = $xr->getAttribute('k'); + $lastValues = array(); + } + + // Begin: /result/field/value + // Begin: /result/field/v + if ($xr->nodeType == XMLReader::ELEMENT && + ($xr->name === 'value' || $xr->name === 'v')) + { + $insideValue = TRUE; + } + + // Read: /result/field/value/text/[TEXT] + // Read: /result/field/v/[TEXT] + if ($insideValue && + $xr->nodeType == XMLReader::TEXT) + { + $lastValues[] = $xr->value; + } + + // End: /result/field/value + // End: /result/field/v + if ($xr->nodeType == XMLReader::END_ELEMENT && + ($xr->name === 'value' || $xr->name === 'v')) + { + $insideValue = FALSE; + } + + // End: /result/field + if ($xr->nodeType == XMLReader::END_ELEMENT && + $xr->name === 'field') + { + if (count($lastValues) === 1) + { + $lastValues = $lastValues[0]; + } + $result[$lastKey] = $lastValues; + } + + // End: /result + if ($xr->nodeType == XMLReader::END_ELEMENT && + $xr->name === 'result') + { + break; + } + } + return $result; + } +} diff --git a/splunk/Splunk/SavedSearch.php b/splunk/Splunk/SavedSearch.php new file mode 100644 index 00000000..e6af88b6 --- /dev/null +++ b/splunk/Splunk/SavedSearch.php @@ -0,0 +1,47 @@ + + * "POST saved/searches/{name}/dispatch" + * endpoint in the REST API Documentation. + * @return Splunk_Job + * The created search job. + */ + public function dispatch($args=array()) + { + $response = $this->sendPost('/dispatch', $args); + $xml = new SimpleXMLElement($response->body); + $sid = Splunk_XmlUtil::getTextContentAtXpath($xml, '/response/sid'); + + return $this->service->getJobs()->getReference( + $sid, $this->getNamespace()); + } +} \ No newline at end of file diff --git a/splunk/Splunk/Service.php b/splunk/Splunk/Service.php new file mode 100644 index 00000000..a0f0d19a --- /dev/null +++ b/splunk/Splunk/Service.php @@ -0,0 +1,123 @@ + + * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to create the entity. Defaults to the service's + * namespace.
+ * }
+ * For details, see the + * + * "POST search/jobs" + * endpoint in the REST API Documentation. + * @return Splunk_Job + * @throws Splunk_IOException + */ + public function search($search, $args=array()) + { + return $this->getJobs()->create($search, $args); + } + + /** + * Executes the specified search query and returns results immediately. + * + * @param string $search The search query for the job to perform. + * @param array $args (optional) Job-specific creation arguments, + * merged with {
+ * **namespace**: (optional) {Splunk_Namespace} The namespace in which + * to create the entity. Defaults to the service's + * namespace.
+ * }
+ * For details, see the + * + * "POST search/jobs" + * endpoint in the REST API Documentation. + * @return string The search results, which can be parsed with + * Splunk_ResultsReader. + * @throws Splunk_IOException + */ + public function oneshotSearch($search, $args=array()) + { + return $this->getJobs()->createOneshot($search, $args); + } +} \ No newline at end of file diff --git a/splunk/Splunk/StreamStream.php b/splunk/Splunk/StreamStream.php new file mode 100644 index 00000000..39ce324e --- /dev/null +++ b/splunk/Splunk/StreamStream.php @@ -0,0 +1,176 @@ +stream = Splunk_StreamStream::$registeredStreams[$streamId]; + $this->streamId = $streamId; + return TRUE; + } + else + { + return FALSE; + } + } + + public function stream_read($count) + { + return fread($this->stream, $count); + } + + public function stream_write($data) + { + return fwrite($this->stream, $data); + } + + public function stream_seek($offset, $whence = SEEK_SET) + { + return fseek($this->stream, $offset, $whence); + } + + public function stream_tell() + { + return ftell($this->stream); + } + + public function stream_eof() + { + return feof($this->stream); + } + + public function stream_stat() + { + return fstat($this->stream); + } + + public function stream_flush() + { + return fflush($this->stream); + } + + public function stream_close() + { + fclose($this->stream); + + // (When called from a shutdown hook, sometimes this variable no + // longer exists when this method is called.) + if (isset(Splunk_StreamStream::$registeredStreams)) + { + unset(Splunk_StreamStream::$registeredStreams[$this->streamId]); + } + } + + public function url_stat($path, $flags) + { + $url = parse_url($path); + $streamId = $url['host']; + + if (array_key_exists($streamId, Splunk_StreamStream::$registeredStreams)) + { + $stream = Splunk_StreamStream::$registeredStreams[$streamId]; + $statResult = fstat($stream); + if ($statResult === FALSE) + { + /* + * The API for url_stat() always requires a valid (non-FALSE), + * result, even though fstat() can return FALSE. The docs say + * to set unknown values to "a rational value (usually 0)". + * + * XMLReader::open() enforces this, printing out a cryptic + * "Unable to open source data" error if a FALSE result for + * url_stat() is returned. + */ + return array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 1, + 'uid' => 0, + 'gid' => 0, + 'rdev' => -1, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => -1, + 'blocks' => -1, + ); + } + else + { + return $statResult; + } + } + else + { + return FALSE; + } + } +} + +stream_wrapper_register('splunkstream', 'Splunk_StreamStream') or + die('Could not register protocol.'); diff --git a/splunk/Splunk/StringStream.php b/splunk/Splunk/StringStream.php new file mode 100644 index 00000000..da00bcff --- /dev/null +++ b/splunk/Splunk/StringStream.php @@ -0,0 +1,51 @@ + $dict without $key + * [1] => $dict[$key] if it exists, or $defaultValue if it does not + * } + */ + public static function extractArgument($dict, $key, $defaultValue) + { + $value = array_key_exists($key, $dict) ? $dict[$key] : $defaultValue; + unset($dict[$key]); + return array($dict, $value); + } + + /** + * Gets the value for the specified $key from the specified $dict, + * returning the $defaultValue in the key is not found. + * + * @param array $dict + * @param mixed $key + * @param mixed $defaultValue + * @return mixed + */ + public static function getArgument($dict, $key, $defaultValue) + { + return array_key_exists($key, $dict) ? $dict[$key] : $defaultValue; + } + + /** + * Writes $data to $stream. + * + * @throws Splunk_IOException If an I/O error occurs. + */ + public static function fwriteall($stream, $data) + { + while (TRUE) + { + $numBytesWritten = fwrite($stream, $data); + if ($numBytesWritten === FALSE) + { + $errorInfo = error_get_last(); + $errmsg = $errorInfo['message']; + $errno = $errorInfo['type']; + throw new Splunk_IOException($errmsg, $errno); + } + if ($numBytesWritten == strlen($data)) + return; + $data = substr($data, $numBytesWritten); + } + } + + /** + * Reads the entire contents of the specified stream. + * Throws a Splunk_IOException upon error. + * + * @param resource $stream A stream. + * @return string The contents of the specified stream. + * @throws Splunk_IOException If an I/O error occurs. + */ + public static function stream_get_contents($stream) + { + // HACK: Clear the last error + @trigger_error(''); + + $oldError = error_get_last(); + $body = @stream_get_contents($stream); + $newError = error_get_last(); + + // HACK: Detecting whether stream_get_contents() has failed is not + // strightforward because it can either return FALSE or ''. + // However '' is also a legal return value in non-error scenarios. + if ($newError != $oldError) + { + $errorInfo = error_get_last(); + $errmsg = $errorInfo['message']; + $errno = $errorInfo['type']; + throw new Splunk_IOException($errmsg, $errno); + } + + return $body; + } +} \ No newline at end of file diff --git a/splunk/Splunk/XmlUtil.php b/splunk/Splunk/XmlUtil.php new file mode 100644 index 00000000..40e44282 --- /dev/null +++ b/splunk/Splunk/XmlUtil.php @@ -0,0 +1,91 @@ +getName() != ''; + } + + /** + * @param SimpleXMLElement $xml + * @param string $attributeName + * @return string|NULL + */ + public static function getAttributeValue($xml, $attributeName) + { + return (isset($xml->attributes()->$attributeName)) + ? (string) $xml->attributes()->$attributeName + : NULL; + } + + /** + * @param SimpleXMLElement $xml + * @return string + */ + public static function getTextContent($xml) + { + // HACK: Some versions of PHP 5 can't access the [0] element + // of a SimpleXMLElement object properly. + return (string) $xml; + } + + /** + * @param SimpleXMLElement $xml + * @param string $xpathExpr + * @return string|NULL + */ + public static function getTextContentAtXpath($xml, $xpathExpr) + { + $matchingElements = $xml->xpath($xpathExpr); + return (count($matchingElements) == 0) + ? NULL + : Splunk_XmlUtil::getTextContent($matchingElements[0]); + } + + /** + * Returns true if the specified SimpleXMLElement represents a unique + * element or false if it represents a collection of elements. + * + * @param SimpleXMLElement $xml + * @return bool + */ + public static function isSingleElement($xml) + { + $count = 0; + foreach ($xml as $item) + { + $count++; + if ($count >= 2) + return false; + } + return ($count == 1); + } +} diff --git a/splunk/order/search.php b/splunk/order/search.php new file mode 100644 index 00000000..57657791 --- /dev/null +++ b/splunk/order/search.php @@ -0,0 +1,373 @@ + IP +// +//{ +// "COLI_WebCode": "JP", +// "COLI_ID": "ZPL161028068", +// "COLI_ApplyDate": "2016-04-15 23:17:20", +// "COLI_SenderIP": "116.226.169.236" +//} +// +//CHT:http://www.chinahighlights.com/api/api.php?method=order.json_by_lmr&order_id=hy161104033 +//国际:http://www.viaje-a-china.com/index.php/ajax/ajax_order_datas/ +// +// +$order_id = isset($_GET['order_id']) ? $_GET['order_id'] : FALSE; +$order_id = isset($_POST['order_id']) ? $_POST['order_id'] : $order_id; +//$site = isset($_GET['site']) ? $_GET['site'] : FALSE; +//$site = isset($_POST['site']) ? $_POST['site'] : $site; +$ip = '255.255.255.255'; +$web_code = FALSE; +$date = date('Y-m-d h:i:s'); +$host = array( + 'xx' => 'xx', + 'jp' => 'JP', + 'vc' => 'FR', + 'vac' => 'ES', + 'train_vac' => 'ES', + 'ru' => 'RU', + 'train_ru' => 'RU', + 'it' => 'IT', + 'train_it' => 'IT', + 'cht' => 'CHT', + 'ct' => 'yincheng', + 'gm' => 'gm-vps', + 'ah' => 'AH', +); +$host_name = array( + 'jp' => 'http://www.arachina.com', + 'vc' => 'http://www.voyageschine.com', + 'vac' => 'http://www.viaje-a-china.com', + 'train_vac' => 'http://www.viaje-a-china.com', + 'ru' => 'http://www.chinahighlights.ru', + 'train_ru' => 'http://www.chinahighlights.ru', + 'it' => 'http://www.viaggio-in-cina.it', + 'train_it' => 'http://www.viaggio-in-cina.it', + 'cht' => 'http://www.chinahighlights.com', + 'ct' => 'http://www.chinatravel.com', + 'gm' => 'http://www.chinarundreisen.com', + 'ah' => 'http://www.asiahighlights.com', +); +$bf_date = date('m/d/Y:00:00:00', time() - 3600*24*90); +$af_date = date('m/d/Y:00:00:00', time() + 3600*24*90); +$order_txt = ''; +$web_code = 'xx'; +$order_date = ''; + +if ($order_id) +{ + //if ($site == 'ch') + //{ + // $order = @file_get_contents('http://www.chinahighlights.com/api/api.php?method=order.json_by_lmr&order_id='.$order_id); + //} + //else + //{ + $order = @file_get_contents('http://www.viaje-a-china.com/index.php/ajax/ajax_order_datas/'.$order_id); + //} + + if ($order && $order !== 'no data') + { + $order = json_decode($order); + $ip = $order->COLI_SenderIP; + if (!$ip || $ip=='159.8.126.74' || $ip=='180.140.114.208') + { + $ip = null; + } + $web_code = $order->COLI_WebCode; + $order_date = $order->COLI_ApplyDate; + $date_time = strtotime($order->COLI_ApplyDate); + $bf_date = date('m/d/Y:00:00:00', $date_time - 3600*24*90); + $af_date = date('m/d/Y:00:00:00', $date_time + 3600*24*90); + $order_txt = $order->COLI_OrderDetailText; + } +} + +//连接Splunk SDK +require_once '../Splunk.php'; +$connect_arguments = array( + 'scheme' => 'https', + 'host' => '192.155.250.125', + 'port' => 8089, + 'username' => 'haina', + 'password' => '383d43GZ82[Ai', +); +$service = new Splunk_Service($connect_arguments); +$service->login(); + +//查询IP +//[EXA] host=ES "68.180.229.232" earliest="11/1/2016:00:00:00" latest="11/2/2016:00:00:00" +$results = array(); +if ($ip) +{ + $search = 'search host='.$host[strtolower($web_code)].' "'.$ip.'" earliest="'.$bf_date.'" latest="'.$af_date.'" NOT("/ngx_pagespeed_beacon" OR "/tourprice" OR "/include" OR "/api" OR "/ajax" OR "/image" OR "/common" OR "/ckplayer" OR ".css" OR ".js" OR ".ico" OR ".gif" OR ".jpg" OR ".png" OR ".json" OR ".xml" OR ".ttf" OR ".woff" OR ".eot" OR "webhtmllog" OR "asphttp_accept_language" OR "down.asp" OR "verifyemail" OR "test")'; + //调用Spluck Search + $job = $service->getJobs()->create($search); + while (!$job->isDone()) + { + $job->refresh(); + } + $results = $job->getResults(); +} + + +?> + + +订单日志查询 + + + + + + + + +
+
+
+ + +
+ +
+
+ + + + getFieldNames(); + //所需字段 + $need_th = array('_serial', '_raw', '_time'); + //字段差集 + $columnNames = array_intersect($need_th, $columnNames); + echo ''; + foreach($columnNames as $columnName) + echo ''; + echo ''; + echo "\n"; + } + else if ($result instanceof Splunk_ResultsMessage) + { + $messages[] = $result; + } + else if (is_array($result)) + { + $anyRows = TRUE; + assert($columnNames !== NULL); + echo ''; + foreach($columnNames as $columnName) + { + $cellValue = array_key_exists($columnName, $result) ? $result[$columnName] : NULL; + //nginx日志 + if ($columnName == '_raw') + { + echo ''; + } + //日期 + else if ($columnName == '_time') + { + echo ''; + } + //splunk记录 + else + { + echo ''; + } + } + echo ''; + } + } + ?> +
提示: “GET”表示正常访问;“POST”表示从表单提交访问;“标黄”表示订单相关的访问。
'.htmlspecialchars($columnName). + '
'; + if (is_array($cellValue)) + { + $log = implode('', $cellValue); + //116.226.169.236 - - [27/Oct/2016:07:35:39 -0500] "GET / HTTP/1.1" 200 8857 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A456 Safari/602.1" + //ip(1), visit(3), from(6), ua(7) + $p = '/^(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})\s-\s-\s\[(.*)\]\s\"(.*)\"\s(\d{3})\s(\d+)\s\"(.*)\"\s\"(.*)\"$/'; + preg_match($p, $log, $matches); + //print_r($matches); + if (isset($matches[3])) + { + $tmp_a = explode(' ', $matches[3]); + echo 'visit '.urldecode($matches[3]).'  '; + } + if (isset($matches[6])) + { + if ($matches[6]=='-') $matches[6] = '从收藏夹、历史记录或地址栏直接访问'; + echo 'from '.urldecode($matches[6]).'  '; + } + if (!isset($matches[1])) + { + echo $log; + } + } + echo '
'; + $time = strtotime($cellValue); + echo date('Y-m-d h:i:s', $time); + echo ''; + if ($cellValue !== NULL) + { + if (is_array($cellValue)) + { + echo '
    '; + foreach($cellValue as $value) + { + echo '
  • '.htmlspecialchars($value).'
  • '; + } + echo '
'; + } + else + { + echo htmlspecialchars($cellValue); + } + } + echo '
+ + 0): ?> +
    + [' . htmlspecialchars($message->getType()) . '] '; + echo htmlspecialchars($message->getText()) . ''; + } + ?> +
+ + +

+ 没有查到相关日志 +

+ + + + + \ No newline at end of file