├── .mailmap ├── AUTHORS ├── COPYING ├── README.md ├── application ├── clicommands │ └── CheckCommand.php ├── controllers │ ├── DocumentsController.php │ ├── EventsController.php │ ├── EventtypesController.php │ └── InstancesController.php ├── forms │ ├── AutorefreshControlForm.php │ ├── EventtypeConfigForm.php │ ├── EventtypeControlForm.php │ └── InstanceConfigForm.php └── views │ ├── helpers │ └── Document.php │ └── scripts │ ├── documents │ └── index.phtml │ ├── events │ ├── create-eventtype.phtml │ └── index.phtml │ ├── eventtypes │ ├── create-instance.phtml │ └── index.phtml │ ├── form.phtml │ └── instances │ └── index.phtml ├── configuration.php ├── doc ├── 01-About.md ├── 02-Installation.md ├── 03-Configuration.md ├── 04-Checks.md └── res │ └── screenshots │ ├── 02-Configuration-New-Event-Type.png │ ├── 02-Configuration-New-Instance.png │ └── 99-Overview.png ├── examples └── event-types.ini ├── library ├── Elasticsearch │ ├── AutorefreshControlWidget.php │ ├── Controller.php │ ├── Elastic.php │ ├── Eventtypes.php │ ├── Exception │ │ └── RestApiException.php │ ├── FilterRenderer.php │ ├── Instances.php │ ├── ProvidedHook │ │ └── Monitoring │ │ │ └── HostActions.php │ ├── Query.php │ └── RestApi │ │ ├── CountApiRequest.php │ │ ├── DeleteApiRequest.php │ │ ├── DocumentApiRequest.php │ │ ├── FilterRenderer.php │ │ ├── GetApiRequest.php │ │ ├── GetIndicesApiRequest.php │ │ ├── GetMappingApiRequest.php │ │ ├── IndexApiRequest.php │ │ ├── MappingApiRequest.php │ │ ├── RestApiClient.php │ │ ├── RestApiQuery.php │ │ ├── RestApiRequest.php │ │ ├── RestApiResponse.php │ │ ├── SearchApiRequest.php │ │ ├── SearchHit.php │ │ └── UpdateApiRequest.php └── vendor │ ├── Psr │ ├── Http │ │ └── Message │ │ │ ├── MessageInterface.php │ │ │ ├── RequestInterface.php │ │ │ ├── ResponseInterface.php │ │ │ ├── ServerRequestInterface.php │ │ │ ├── StreamInterface.php │ │ │ ├── UploadedFileInterface.php │ │ │ └── UriInterface.php │ ├── LICENSE │ └── Loader.php │ └── iplx │ ├── Http │ ├── Client.php │ ├── ClientInterface.php │ ├── Handle.php │ ├── MessageTrait.php │ ├── Request.php │ ├── Response.php │ ├── Stream.php │ └── Uri.php │ ├── LICENSE │ ├── Loader.php │ └── README ├── module.info ├── public └── css │ └── module.less └── run.php /.mailmap: -------------------------------------------------------------------------------- 1 | Markus Frosch Markus Frosch 2 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Markus Frosch 2 | Thomas Gelf 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :warning: Deprecated :warning: 2 | 3 | This module will not be updated by Icinga anymore. Please don't attempt to use it. 4 | 5 | # Elasticsearch Module for Icinga Web 2 6 | 7 | ![Icinga Logo](https://www.icinga.com/wp-content/uploads/2014/06/icinga_logo.png) 8 | 9 | 1. [About](#about) 10 | 2. [Requirements](#requirements) 11 | 3. [License](#license) 12 | 4. [Getting Started](#getting-started) 13 | 5. [Documentation](#documentation) 14 | 6. [Contributing](#contributing) 15 | 16 | ## About 17 | 18 | The Elasticsearch Module for Icinga Web 2 integrates your [Elastic](https://www.elastic.co/) stack into 19 | [Icinga Web 2](https://www.icinga.org/products/icinga-web-2/). Based on 20 | [Elasticsearch](https://www.elastic.co/products/elasticsearch) instances and event types you configure, the module 21 | allows you to display data collected by [Beats](https://www.elastic.co/products/beats), 22 | [Logstash](https://www.elastic.co/products/logstash) and any other source. After you've installed and configured the 23 | module, you can browse events via the host action **Elasticsearch Events**. 24 | 25 | It also brings a command for `icingacli`which can be used to query Elasticsearch for certain events. This command 26 | can be used to create Icinga 2 checks. 27 | 28 | ![Icinga Web 2 Module Elasticsearch](doc/res/screenshots/99-Overview.png) 29 | 30 | ## Requirements 31 | 32 | * Icinga Web 2 version 2.4.2+ 33 | * PHP version 5.6.x or 7.x 34 | * php-curl 35 | 36 | ## License 37 | 38 | The Elasticsearch Module for Icinga Web 2 is licensed under the terms of the GNU 39 | General Public License Version 2, you will find a copy of this license in the 40 | [COPYING](COPYING) file included in the source package. 41 | 42 | ## Getting Started 43 | 44 | Please have a look at our [installation instructions](doc/02-Installation.md) and how to 45 | [configure](doc/03-Configuration.md) the module. 46 | 47 | For running checks you can refer to the [checks](doc/04-Checks.md) chapter. 48 | 49 | ## Documentation 50 | 51 | A complete list of all our documentation can be found in the [doc](doc/) directory. 52 | 53 | ## Contributing 54 | 55 | There are many ways to contribute to Icinga -- whether it be sending patches, 56 | testing, reporting bugs, or reviewing and updating the documentation. Every 57 | contribution is appreciated! 58 | -------------------------------------------------------------------------------- /application/clicommands/CheckCommand.php: -------------------------------------------------------------------------------- 1 | 'OK', 16 | 1 => 'WARNING', 17 | 2 => 'CRITICAL', 18 | 3 => 'UNKNOWN' 19 | ]; 20 | 21 | /** 22 | * Count Elasticsearch events in a certain time period and report warning or critical according to the given thresholds 23 | * 24 | * USAGE: 25 | * 26 | * icingacli elasticsearch check [options] 27 | * 28 | * OPTIONS: 29 | * 30 | * --instance Name of the configure Elasticsearch instace 31 | * 32 | * --index Elasticsearch index pattern 33 | * 34 | * --filter Elasticsearch filter in the Icinga Web 2 URL filter format 35 | * 36 | * --crit Critical threshold 37 | * 38 | * --warn Warning threshold 39 | * 40 | * --from English textual representation of the start timestamp. Defaults to -5m. 41 | * 42 | * Note: these options are the same as in the web frontend! 43 | */ 44 | public function defaultAction() 45 | { 46 | $instance = (new Instances()) 47 | ->select() 48 | ->where('name', $this->params->getRequired('instance')) 49 | ->fetchRow(); 50 | 51 | if ($instance === false) { 52 | $this->exitPlugin(3, 'Instance not found'); 53 | } 54 | 55 | $crit = (int) $this->params->getRequired('crit'); 56 | $warn = (int) $this->params->getRequired('warn'); 57 | 58 | $index = $this->params->getRequired('index'); 59 | 60 | $filter = Filter::matchAll( 61 | Filter::expression('@timestamp', '>=', 'now' . $this->params->get('from', '-5m')), 62 | Filter::fromQueryString($this->params->getRequired('filter')) 63 | ); 64 | 65 | $agg = [ 66 | 'aggs' => [ 67 | 'count' => [ 68 | 'value_count' => [ 69 | 'field' => '@timestamp' 70 | ] 71 | ] 72 | ] 73 | ]; 74 | 75 | try { 76 | $response = (new Elastic($instance)) 77 | ->select() 78 | ->from($index) 79 | ->filter((new FilterRenderer($filter))->getQuery()) 80 | ->patch($agg) 81 | ->limit(0) 82 | ->getResponse(); 83 | } catch (Exception $e) { 84 | $this->exitPlugin(3, $e->getMessage()); 85 | } 86 | 87 | /** @var array $response */ 88 | $count = $response['aggregations']['count']['value']; 89 | 90 | $message = sprintf('%d hits', $count); 91 | 92 | if ($count >= $crit) { 93 | $this->exitPlugin(2, $message); 94 | } 95 | 96 | if ($count >= $warn) { 97 | $this->exitPlugin(1, $message); 98 | } 99 | 100 | $this->exitPlugin(0, $message); 101 | } 102 | 103 | public function exitPlugin($status, $message) 104 | { 105 | printf('%s - %s%s', self::$states[$status], $message, PHP_EOL); 106 | 107 | exit($status); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /application/controllers/DocumentsController.php: -------------------------------------------------------------------------------- 1 | params->getRequired('index'); 15 | $type = $this->params->getRequired('type'); 16 | $id = $this->params->getRequired('id'); 17 | 18 | $instance = (new Instances()) 19 | ->select() 20 | ->where('name', $this->params->getRequired('instance')) 21 | ->fetchRow(); 22 | 23 | if ($instance === false) { 24 | $this->httpNotFound($this->translate('Instance not found')); 25 | } 26 | 27 | $this->setTitle($this->translate('Document')); 28 | 29 | $document = (new Elastic($instance)) 30 | ->select() 31 | ->get("{$index}/{$type}/{$id}"); 32 | 33 | $this->view->document = $document; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /application/controllers/EventsController.php: -------------------------------------------------------------------------------- 1 | params->getRequired('host')); 25 | // $this->applyRestriction('monitoring/filter/objects', $host); 26 | if ($host->fetch() === false) { 27 | $this->httpNotFound($this->translate('Host not found or not permitted')); 28 | } 29 | 30 | $host->populate(); 31 | 32 | $eventtypesRepo = new Eventtypes(); 33 | 34 | if (! (new Instances())->select()->hasResult()) { 35 | $this->setTitle($this->translate('Events')); 36 | 37 | $this->_helper->viewRenderer->setRender('eventtypes/create-instance', null, true); 38 | 39 | return; 40 | } 41 | 42 | if (! $eventtypesRepo->select()->hasResult()) { 43 | $this->setTitle($this->translate('Events')); 44 | 45 | $this->_helper->viewRenderer->setRender('create-eventtype'); 46 | 47 | return; 48 | } 49 | 50 | $eventtypeForm = new EventtypeControlForm(); 51 | $eventtypeForm->handleRequest(); 52 | 53 | $this->view->eventtypeForm = $eventtypeForm; 54 | 55 | $eventtypeFilter = Filter::where( 56 | 'name', 57 | $this->params->get('eventtype', $eventtypesRepo->select(['name'])->fetchRow()->name) 58 | ); 59 | 60 | $allowedTypes = array_reduce( 61 | $this->getRestrictions('elasticsearch/eventtypes'), 62 | function (FilterOr $carry, $item) { 63 | foreach (StringHelper::trimSplit($item) as $eventtype) { 64 | return $carry->orFilter(Filter::where('name', $eventtype)); 65 | } 66 | }, 67 | Filter::matchAny() 68 | ); 69 | 70 | $eventtype = $eventtypesRepo 71 | ->select() 72 | ->applyFilter(! $allowedTypes->isEmpty() ? Filter::matchAll($eventtypeFilter, $allowedTypes) : $eventtypeFilter) 73 | ->fetchRow(); 74 | 75 | if ($eventtype === false) { 76 | $this->httpNotFound($this->translate('Event type not found or not permitted')); 77 | } 78 | 79 | $this->setTitle(sprintf($this->translate('%s Events'), $eventtype->name)); 80 | 81 | $instance = (new Instances()) 82 | ->select() 83 | ->where('name', $eventtype->instance) 84 | ->fetchRow(); 85 | 86 | if ($instance === false) { 87 | $this->httpNotFound($this->translate('Instance for the event type not found')); 88 | } 89 | 90 | $filterString = preg_replace_callback('/\{([\w._]+)\}/', function ($match) use ($host) { 91 | return Macro::resolveMacro($match[1], $host); 92 | }, $eventtype->filter); 93 | 94 | $elasticFilter = new FilterRenderer(Filter::fromQueryString($filterString)); 95 | 96 | $query = (new Elastic($instance)) 97 | ->select(StringHelper::trimSplit($eventtype->fields)) 98 | ->from($eventtype->index) 99 | ->filter($elasticFilter->getQuery()); 100 | 101 | $this->paginate($query); 102 | 103 | $this->setupAutorefreshControl(10); 104 | 105 | $this->view->documentsUri = Url::fromPath('elasticsearch/documents', array('instance' => $eventtype->instance)); 106 | $this->view->events = $query->fetchAll(); 107 | $this->view->fields = $query->getFields(); 108 | $this->view->host = $host; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /application/controllers/EventtypesController.php: -------------------------------------------------------------------------------- 1 | assertPermission('elasticsearch/config'); 18 | } 19 | 20 | public function indexAction() 21 | { 22 | $this->getTabs()->add(uniqid(), [ 23 | 'label' => $this->translate('Instances'), 24 | 'url' => Url::fromPath('elasticsearch/instances') 25 | ]); 26 | 27 | $this->setTitle($this->translate('Event Types')); 28 | 29 | if (! (new Instances())->select()->hasResult()) { 30 | $this->_helper->viewRenderer->setRender('create-instance'); 31 | 32 | return; 33 | } 34 | 35 | $this->view->eventtypes = (new Eventtypes())->select(['name', 'instance', 'index', 'filter', 'fields']); 36 | } 37 | 38 | public function newAction() 39 | { 40 | $form = new EventtypeConfigForm([ 41 | 'mode' => EventtypeConfigForm::MODE_INSERT 42 | ]); 43 | 44 | $form->handleRequest(); 45 | 46 | $this->setTitle($this->translate('New Event Type')); 47 | 48 | $this->view->form = $form; 49 | 50 | $this->_helper->viewRenderer->setRender('form', null, true); 51 | } 52 | 53 | public function updateAction() 54 | { 55 | $name = $this->params->getRequired('eventtype'); 56 | 57 | $form = new EventtypeConfigForm([ 58 | 'mode' => EventtypeConfigForm::MODE_UPDATE, 59 | 'identifier' => $name 60 | ]); 61 | 62 | $form->handleRequest(); 63 | 64 | $this->setTitle($this->translate('Update Event Type')); 65 | 66 | $this->view->form = $form; 67 | 68 | $this->_helper->viewRenderer->setRender('form', null, true); 69 | } 70 | 71 | public function deleteAction() 72 | { 73 | $name = $this->params->getRequired('eventtype'); 74 | 75 | $form = new EventtypeConfigForm([ 76 | 'mode' => EventtypeConfigForm::MODE_DELETE, 77 | 'identifier' => $name 78 | ]); 79 | 80 | $form->handleRequest(); 81 | 82 | $this->setTitle($this->translate('Remove Event Type')); 83 | 84 | $this->view->form = $form; 85 | 86 | $this->_helper->viewRenderer->setRender('form', null, true); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /application/controllers/InstancesController.php: -------------------------------------------------------------------------------- 1 | assertPermission('elasticsearch/config'); 17 | } 18 | 19 | public function indexAction() 20 | { 21 | $this->setTitle($this->translate('Instances')); 22 | 23 | $this->getTabs()->add(uniqid(), [ 24 | 'label' => $this->translate('Event Types'), 25 | 'url' => Url::fromPath('elasticsearch/eventtypes') 26 | ]); 27 | 28 | $this->view->instances = (new Instances())->select(['name', 'uri']); 29 | $this->view->noEventtypes = ! (new Eventtypes())->select()->hasResult(); 30 | } 31 | 32 | public function newAction() 33 | { 34 | $form = new InstanceConfigForm([ 35 | 'mode' => InstanceConfigForm::MODE_INSERT 36 | ]); 37 | 38 | $form->handleRequest(); 39 | 40 | $this->setTitle($this->translate('New Instance')); 41 | 42 | $this->view->form = $form; 43 | 44 | $this->_helper->viewRenderer->setRender('form', null, true); 45 | } 46 | 47 | public function updateAction() 48 | { 49 | $name = $this->params->getRequired('instance'); 50 | 51 | $form = new InstanceConfigForm([ 52 | 'mode' => InstanceConfigForm::MODE_UPDATE, 53 | 'identifier' => $name 54 | ]); 55 | 56 | $form->handleRequest(); 57 | 58 | $this->setTitle($this->translate('Update Instance')); 59 | 60 | $this->view->form = $form; 61 | 62 | $this->_helper->viewRenderer->setRender('form', null, true); 63 | } 64 | 65 | public function deleteAction() 66 | { 67 | $name = $this->params->getRequired('instance'); 68 | 69 | $form = new InstanceConfigForm([ 70 | 'mode' => InstanceConfigForm::MODE_DELETE, 71 | 'identifier' => $name 72 | ]); 73 | 74 | $form->handleRequest(); 75 | 76 | $this->setTitle($this->translate('Remove Instance')); 77 | 78 | $this->view->form = $form; 79 | 80 | $this->_helper->viewRenderer->setRender('form', null, true); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /application/forms/AutorefreshControlForm.php: -------------------------------------------------------------------------------- 1 | '1s', 34 | 10 => '10s', 35 | 30 => '30s', 36 | 60 => '1m' 37 | ); 38 | 39 | public function init() 40 | { 41 | $this->setAttrib('class', static::CSS_CLASS_AUTOREFRESH); 42 | } 43 | 44 | /** 45 | * Get the auto-refresh interval 46 | * 47 | * @return int 48 | */ 49 | public function getInterval() 50 | { 51 | return $this->interval; 52 | } 53 | 54 | /** 55 | * Set the auto-refresh interval 56 | * 57 | * @param int $interval 58 | * 59 | * @return $this 60 | */ 61 | public function setInterval($interval) 62 | { 63 | $this->interval = $interval; 64 | 65 | return $this; 66 | } 67 | 68 | public function getRedirectUrl() 69 | { 70 | return $this->getRequest()->getUrl() 71 | ->setParam('refresh', $this->getElement('refresh')->getValue()); 72 | } 73 | 74 | public function createElements(array $formData) 75 | { 76 | $intervals = static::$intervals; 77 | 78 | $value = $this->getInterval(); 79 | 80 | if (! isset($intervals[$value])) { 81 | $intervals[$value] = "{$value}s"; 82 | } 83 | 84 | $this->addElement( 85 | 'select', 86 | 'refresh', 87 | array( 88 | 'autosubmit' => true, 89 | 'label' => $this->getView()->icon('cw'), 90 | 'multiOptions' => $intervals, 91 | 'value' => $value 92 | ) 93 | ); 94 | 95 | $this->getElement('refresh')->getDecorator('label')->setOption('escape', false); 96 | } 97 | 98 | /** 99 | * Auto-refresh control is always successful 100 | * 101 | * @return bool 102 | */ 103 | public function onSuccess() 104 | { 105 | return true; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /application/forms/EventtypeConfigForm.php: -------------------------------------------------------------------------------- 1 | repository = new Eventtypes(); 19 | $this->redirectUrl = 'elasticsearch/eventtypes'; 20 | } 21 | 22 | /** 23 | * Set the identifier 24 | * 25 | * @param string $identifier 26 | * 27 | * @return $this 28 | */ 29 | public function setIdentifier($identifier) 30 | { 31 | $this->identifier = $identifier; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * Set the mode of the form 38 | * 39 | * @param int $mode 40 | * 41 | * @return $this 42 | */ 43 | public function setMode($mode) 44 | { 45 | $this->mode = $mode; 46 | 47 | return $this; 48 | } 49 | 50 | protected function onUpdateSuccess() 51 | { 52 | if ($this->getElement('btn_remove')->isChecked()) { 53 | $this->setRedirectUrl("elasticsearch/eventtypes/delete?eventtype={$this->getIdentifier()}"); 54 | $success = true; 55 | } else { 56 | $success = parent::onUpdateSuccess(); 57 | } 58 | 59 | return $success; 60 | } 61 | 62 | protected function createBaseElements(array $formData) 63 | { 64 | $this->addElement( 65 | 'text', 66 | 'name', 67 | array( 68 | 'description' => $this->translate('Name of the Event type'), 69 | 'label' => $this->translate('Event Type Name'), 70 | 'placeholder' => 'Elasticsearch', 71 | 'required' => true 72 | ) 73 | ); 74 | 75 | $this->addElement( 76 | 'select', 77 | 'instance', 78 | array( 79 | 'description' => $this->translate('Elasticsearch instance'), 80 | 'label' => $this->translate('Elasticsearch Instance'), 81 | 'multiOptions' => (new Instances())->select(['name', 'name'])->fetchPairs(), 82 | 'required' => true 83 | ) 84 | ); 85 | 86 | $this->addElement( 87 | 'text', 88 | 'index', 89 | array( 90 | 'description' => $this->translate('Elasticsearch index pattern'), 91 | 'label' => $this->translate('Index'), 92 | 'required' => true 93 | ) 94 | ); 95 | 96 | $this->addElement( 97 | 'text', 98 | 'filter', 99 | array( 100 | 'description' => $this->translate('Elasticsearch filter in the Icinga Web 2 filter format'), 101 | 'label' => $this->translate('Filter'), 102 | 'required' => true 103 | ) 104 | ); 105 | 106 | $this->addElement( 107 | 'text', 108 | 'fields', 109 | array( 110 | 'description' => $this->translate( 111 | 'Comma-separated list of field names to display. The @timestamp field is always included' 112 | ), 113 | 'label' => $this->translate('Fields'), 114 | 'required' => true 115 | ) 116 | ); 117 | } 118 | 119 | protected function createInsertElements(array $formData) 120 | { 121 | $this->createBaseElements($formData); 122 | 123 | $this->setTitle($this->translate('Create a New Event Type')); 124 | 125 | $this->setSubmitLabel($this->translate('Save')); 126 | } 127 | 128 | protected function createUpdateElements(array $formData) 129 | { 130 | $this->createBaseElements($formData); 131 | 132 | $this->setTitle(sprintf($this->translate('Update Event Type %s'), $this->getIdentifier())); 133 | 134 | $this->addElement( 135 | 'submit', 136 | 'btn_submit', 137 | [ 138 | 'decorators' => ['ViewHelper'], 139 | 'ignore' => true, 140 | 'label' => $this->translate('Save') 141 | ] 142 | ); 143 | 144 | $this->addElement( 145 | 'submit', 146 | 'btn_remove', 147 | [ 148 | 'decorators' => ['ViewHelper'], 149 | 'ignore' => true, 150 | 'label' => $this->translate('Remove') 151 | ] 152 | ); 153 | 154 | $this->addDisplayGroup( 155 | ['btn_submit', 'btn_remove'], 156 | 'form-controls', 157 | [ 158 | 'decorators' => [ 159 | 'FormElements', 160 | ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']] 161 | ] 162 | ] 163 | ); 164 | } 165 | 166 | protected function createDeleteElements(array $formData) 167 | { 168 | $this->setTitle(sprintf($this->translate('Remove Event Type %s'), $this->getIdentifier())); 169 | 170 | $this->setSubmitLabel($this->translate('Yes')); 171 | } 172 | 173 | protected function createFilter() 174 | { 175 | return Filter::where('name', $this->getIdentifier()); 176 | } 177 | 178 | protected function getInsertMessage($success) 179 | { 180 | return $success 181 | ? $this->translate('Event type created') 182 | : $this->translate('Failed to create Event type'); 183 | } 184 | 185 | protected function getUpdateMessage($success) 186 | { 187 | return $success 188 | ? $this->translate('Event type updated') 189 | : $this->translate('Failed to update Event type'); 190 | } 191 | 192 | protected function getDeleteMessage($success) 193 | { 194 | return $success 195 | ? $this->translate('Event type removed') 196 | : $this->translate('Failed to remove Event type'); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /application/forms/EventtypeControlForm.php: -------------------------------------------------------------------------------- 1 | setAttrib('class', 'eventtype-control'); 14 | $this->setAttrib('data-base-target', '_self'); 15 | } 16 | 17 | public function createElements(array $formData) 18 | { 19 | $this->addElement( 20 | 'select', 21 | 'eventtype', 22 | array( 23 | 'autosubmit' => true, 24 | 'label' => $this->translate('Event Type'), 25 | 'multiOptions' => (new Eventtypes())->select(['name', 'name'])->fetchPairs(), 26 | 'value' => $this->getRequest()->getUrl()->getParam('eventtype', '') 27 | ) 28 | ); 29 | } 30 | 31 | public function getRedirectUrl() 32 | { 33 | return $this->getRequest()->getUrl() 34 | ->setParam('eventtype', $this->getElement('eventtype')->getValue()); 35 | } 36 | 37 | /** 38 | * Control is always successful 39 | * 40 | * @return bool 41 | */ 42 | public function onSuccess() 43 | { 44 | return true; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /application/forms/InstanceConfigForm.php: -------------------------------------------------------------------------------- 1 | repository = new Instances(); 18 | $this->redirectUrl = 'elasticsearch/instances'; 19 | } 20 | 21 | /** 22 | * Set the identifier 23 | * 24 | * @param string $identifier 25 | * 26 | * @return $this 27 | */ 28 | public function setIdentifier($identifier) 29 | { 30 | $this->identifier = $identifier; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Set the mode of the form 37 | * 38 | * @param int $mode 39 | * 40 | * @return $this 41 | */ 42 | public function setMode($mode) 43 | { 44 | $this->mode = $mode; 45 | 46 | return $this; 47 | } 48 | 49 | protected function onUpdateSuccess() 50 | { 51 | if ($this->getElement('btn_remove')->isChecked()) { 52 | $this->setRedirectUrl("elasticsearch/instances/delete?instance={$this->getIdentifier()}"); 53 | $success = true; 54 | } else { 55 | $success = parent::onUpdateSuccess(); 56 | } 57 | 58 | return $success; 59 | } 60 | 61 | protected function createBaseElements(array $formData) 62 | { 63 | $this->addElement( 64 | 'text', 65 | 'name', 66 | array( 67 | 'description' => $this->translate('Name of the Elasticsearch instance'), 68 | 'label' => $this->translate('Instance Name'), 69 | 'placeholder' => 'Elasticsearch', 70 | 'required' => true 71 | ) 72 | ); 73 | 74 | $this->addElement( 75 | 'text', 76 | 'uri', 77 | array( 78 | 'description' => $this->translate('URI to the Elasticsearch instance'), 79 | 'label' => $this->translate('URI'), 80 | 'placeholder' => 'http://localhost:9200', 81 | 'required' => true 82 | ) 83 | ); 84 | 85 | $this->addElement( 86 | 'text', 87 | 'user', 88 | array( 89 | 'description' => $this->translate('The user to use for authentication'), 90 | 'label' => $this->translate('User') 91 | ) 92 | ); 93 | 94 | $this->addElement( 95 | 'password', 96 | 'password', 97 | array( 98 | 'description' => $this->translate('The password to use for authentication'), 99 | 'label' => $this->translate('Password'), 100 | 'renderPassword' => true 101 | ) 102 | ); 103 | $this->addElement( 104 | 'text', 105 | 'ca', 106 | array( 107 | 'description' => $this->translate( 108 | 'The path of the file containing one or more certificates to verify the peer with or the path' 109 | . ' to the directory that holds multiple CA certificates'), 110 | 'label' => $this->translate('Certificate Authority') 111 | ) 112 | ); 113 | $this->addElement( 114 | 'text', 115 | 'client_certificate', 116 | array( 117 | 'description' => $this->translate('The path of the client certificate'), 118 | 'label' => $this->translate('Client Certificate') 119 | ) 120 | ); 121 | $this->addElement( 122 | 'text', 123 | 'client_private_key', 124 | array( 125 | 'description' => $this->translate('The path of the client private key'), 126 | 'label' => $this->translate('Client Private Key') 127 | ) 128 | ); 129 | } 130 | 131 | protected function createInsertElements(array $formData) 132 | { 133 | $this->createBaseElements($formData); 134 | 135 | $this->setTitle($this->translate('Create a New Instance')); 136 | 137 | $this->setSubmitLabel($this->translate('Save')); 138 | } 139 | 140 | protected function createUpdateElements(array $formData) 141 | { 142 | $this->createBaseElements($formData); 143 | 144 | $this->setTitle(sprintf($this->translate('Update Instance %s'), $this->getIdentifier())); 145 | 146 | $this->addElement( 147 | 'submit', 148 | 'btn_submit', 149 | [ 150 | 'decorators' => ['ViewHelper'], 151 | 'ignore' => true, 152 | 'label' => $this->translate('Save') 153 | ] 154 | ); 155 | 156 | $this->addElement( 157 | 'submit', 158 | 'btn_remove', 159 | [ 160 | 'decorators' => ['ViewHelper'], 161 | 'ignore' => true, 162 | 'label' => $this->translate('Remove') 163 | ] 164 | ); 165 | 166 | $this->addDisplayGroup( 167 | ['btn_submit', 'btn_remove'], 168 | 'form-controls', 169 | [ 170 | 'decorators' => [ 171 | 'FormElements', 172 | ['HtmlTag', ['tag' => 'div', 'class' => 'control-group form-controls']] 173 | ] 174 | ] 175 | ); 176 | } 177 | 178 | protected function createDeleteElements(array $formData) 179 | { 180 | $this->setTitle(sprintf($this->translate('Remove Instance %s'), $this->getIdentifier())); 181 | 182 | $this->setSubmitLabel($this->translate('Yes')); 183 | } 184 | 185 | protected function createFilter() 186 | { 187 | return Filter::where('name', $this->getIdentifier()); 188 | } 189 | 190 | protected function getInsertMessage($success) 191 | { 192 | return $success 193 | ? $this->translate('Elasticsearch instance created') 194 | : $this->translate('Failed to create Elasticsearch instance'); 195 | } 196 | 197 | protected function getUpdateMessage($success) 198 | { 199 | return $success 200 | ? $this->translate('Elasticsearch instance updated') 201 | : $this->translate('Failed to update Elasticsearch instance'); 202 | } 203 | 204 | protected function getDeleteMessage($success) 205 | { 206 | return $success 207 | ? $this->translate('Elasticsearch instance removed') 208 | : $this->translate('Failed to remove Elasticsearch instance'); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /application/views/helpers/Document.php: -------------------------------------------------------------------------------- 1 | ']; 9 | 10 | foreach ($document as $key => $value) { 11 | $html[] = ''; 12 | 13 | $html[] = ''; 14 | $html[] = $this->view->escape($key); 15 | $html[] = ''; 16 | 17 | if (is_array($value)) { 18 | $html[] = ''; 19 | $html[] = $this->document($value); 20 | } else { 21 | $html[] = ''; 22 | $html[] = $this->view->escape($value); 23 | } 24 | 25 | $html[] = ''; 26 | } 27 | 28 | $html[] = ''; 29 | 30 | return implode("\n", $html); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /application/views/scripts/documents/index.phtml: -------------------------------------------------------------------------------- 1 | compact): ?> 2 |
3 | 4 |
5 | 6 |
7 | document($document) ?> 8 |
9 | -------------------------------------------------------------------------------- /application/views/scripts/events/create-eventtype.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |

translate('No event types configured yet. You have to configure event types in order to access data in Elasticsearch.') ?>

8 |
9 | qlink( 10 | $this->translate('Create a New Event Type') , 11 | 'elasticsearch/eventtypes/new', 12 | null, 13 | [ 14 | 'class' => 'button-link', 15 | 'data-base-target' => '_next', 16 | 'icon' => 'plus', 17 | 'title' => $this->translate('Create a new event type') 18 | ] 19 | ) ?> 20 |
21 |
22 | -------------------------------------------------------------------------------- /application/views/scripts/events/index.phtml: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | partial('partials/object/host-header.phtml', 'monitoring', ['object' => $host]) ?> 5 |
6 | 7 | 8 | 9 |
10 |
11 | 12 |

translate('No events found.') ?>

13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ellipsis($column, 100); 29 | if ($column !== $ellipsis): ?> 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
escape($field) ?>
escape($ellipsis) ?>escape($column) ?>
39 | 40 | -------------------------------------------------------------------------------- /application/views/scripts/eventtypes/create-instance.phtml: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | 6 |
7 |

8 | translate('No Elasticsearch instance configured yet. You have to configure an instance before creating event types.') ?> 9 |

10 |
11 | qlink( 12 | $this->translate('Create a New Instance') , 13 | 'elasticsearch/instances/new', 14 | null, 15 | [ 16 | 'class' => 'button-link', 17 | 'data-base-target' => '_self', 18 | 'icon' => 'plus', 19 | 'title' => $this->translate('Create a new instance') 20 | ] 21 | ) ?> 22 |
23 |
24 | -------------------------------------------------------------------------------- /application/views/scripts/eventtypes/index.phtml: -------------------------------------------------------------------------------- 1 | compact): ?> 2 |
3 | tabs ?> 4 |
5 | 6 |
7 | hasResult()): ?> 8 |

translate('No event types configured yet. You have to configure event types in order to access data in Elasticsearch.') ?>

9 | 10 |
11 | qlink( 12 | $this->translate('Create a New Event Type') , 13 | 'elasticsearch/eventtypes/new', 14 | null, 15 | [ 16 | 'class' => 'button-link', 17 | 'data-base-target' => '_next', 18 | 'icon' => 'plus', 19 | 'title' => $this->translate('Create a new event type') 20 | ] 21 | ) ?> 22 |
23 | hasResult()): ?> 24 |
25 | 26 |
27 |

escape($eventtype->name) ?>

28 |
29 |
translate('Name') ?>
30 |
escape($eventtype->name) ?>
31 |
translate('Instance') ?>
32 |
escape($eventtype->instance) ?>
33 |
translate('Index') ?>
34 |
escape($eventtype->index) ?>
35 |
translate('Filter') ?>
36 |
escape($eventtype->filter) ?>
37 |
translate('Fields') ?>
38 |
escape($eventtype->fields) ?>
39 |
40 | qlink( 41 | $this->translate('Update Event Type') , 42 | 'elasticsearch/eventtypes/update', 43 | [ 44 | 'eventtype' => $eventtype->name 45 | ], 46 | [ 47 | 'class' => 'button-link', 48 | 'data-base-target' => '_next', 49 | 'icon' => 'plus', 50 | 'title' => $this->translate('Update event type') 51 | ] 52 | ) ?> 53 |
54 | 55 |
56 | 57 |
58 | -------------------------------------------------------------------------------- /application/views/scripts/form.phtml: -------------------------------------------------------------------------------- 1 | compact): ?> 2 |
3 | tabs ?> 4 |
5 | 6 |
7 | form ?> 8 |
9 | -------------------------------------------------------------------------------- /application/views/scripts/instances/index.phtml: -------------------------------------------------------------------------------- 1 | compact): ?> 2 |
3 | tabs ?> 4 |
5 | 6 |
7 |
8 | qlink( 9 | $this->translate('Create a New Instance') , 10 | 'elasticsearch/instances/new', 11 | null, 12 | [ 13 | 'class' => 'button-link', 14 | 'data-base-target' => '_next', 15 | 'icon' => 'plus', 16 | 'title' => $this->translate('Create a new instance') 17 | ] 18 | ) ?> 19 |
20 | hasResult()): ?> 21 |

translate('No Elasticsearch instance configured yet. You have to configure an instance before creating event types.') ?>

22 | 23 | 24 |

translate('No event types configured yet. You have to configure event types in order to access data in Elasticsearch.') ?>

25 |
26 | qlink( 27 | $this->translate('Create a New Event Type') , 28 | 'elasticsearch/eventtypes/new', 29 | null, 30 | [ 31 | 'class' => 'button-link', 32 | 'data-base-target' => '_self', 33 | 'icon' => 'plus', 34 | 'title' => $this->translate('Create a new event type') 35 | ] 36 | ) ?> 37 |
38 | 39 |
40 | 41 |
42 |

escape($instance->name) ?>

43 |
44 |
translate('Name') ?>
45 |
escape($instance->name) ?>
46 |
translate('URI') ?>
47 |
escape($instance->uri) ?>
48 |
49 | qlink( 50 | $this->translate('Update Instance') , 51 | 'elasticsearch/instances/update', 52 | [ 53 | 'instance' => $instance->name 54 | ], 55 | [ 56 | 'class' => 'button-link', 57 | 'data-base-target' => '_next', 58 | 'icon' => 'plus', 59 | 'title' => $this->translate('Update Instance') 60 | ] 61 | ) ?> 62 |
63 | 64 |
65 | 66 |
67 | -------------------------------------------------------------------------------- /configuration.php: -------------------------------------------------------------------------------- 1 | providePermission( 7 | 'elasticsearch/config', 8 | $this->translate('Allow to configure Elasticsearch instances and event types') 9 | ); 10 | 11 | $this->providePermission( 12 | 'elasticsearch/events', 13 | $this->translate('Allow access to view Elasticsearch events on a host') 14 | ); 15 | 16 | $this->provideRestriction( 17 | 'elasticsearch/eventtypes', 18 | $this->translate('Restrict the event types the user may use') 19 | ); 20 | 21 | $this->provideConfigTab('elasticsearch/instances', array( 22 | 'title' => $this->translate('Configure Elasticsearch Instances'), 23 | 'label' => $this->translate('Elasticsearch Instances'), 24 | 'url' => 'instances' 25 | )); 26 | 27 | $this->provideConfigTab('elasticsearch/eventtypes', array( 28 | 'title' => $this->translate('Configure Event Types'), 29 | 'label' => $this->translate('Event Types'), 30 | 'url' => 'eventtypes' 31 | )); 32 | -------------------------------------------------------------------------------- /doc/01-About.md: -------------------------------------------------------------------------------- 1 | # Icinga Web2 Elasticsearch Module 2 | 3 | Elasticsearch is a powerful search engine based on Apache Lucene. It stores and indexes data and provides access to it 4 | via a REST API. Elasticsearch often is used to store logging data, received from a central log 5 | management software such as Logstash, Filebeat or Graylog. 6 | 7 | The Elasticsearch module for Icinga Web 2 gives you access to this data, embedded in your Icinga Web 2 interface. 8 | Custom filters allow you to limit the data that should be displayed. You can give your users access to 9 | certain data types without revealing everything stored in Elasticsearch. Multiple Elasticsearch instances can be 10 | configured and accessed either without authentication, HTTP basic authentication or certificates. 11 | 12 | Read the [installation instructions](02-Installation.md) to get started. -------------------------------------------------------------------------------- /doc/02-Installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | * Icinga Web 2 >= 2.4.2 6 | * PHP version 5.6.x or 7.x 7 | * php-curl 8 | * Elasticsearch >= 5.x 9 | 10 | ## Installation 11 | As with any Icinga Web 2 module, installation is pretty straight-forward. You just have to drop the module into the 12 | `/usr/share/icingaweb2/modules/elasticsearch` directory. Please note that the directory name **must** be `elasticsearch` 13 | and nothing else. If you want to use a different directory, make sure it is within the module path of Icinga Web 2. 14 | 15 | ```shell 16 | git clone https://github.com/icinga/icingaweb2-module-elasticsearch.git /usr/share/icingaweb2/modules/elasticsearch 17 | ``` 18 | 19 | The module can be enabled through the web interface (`Configuration -> Modules -> elasticsearch`) or via the CLI: 20 | 21 | ```shell 22 | icingacli module enable elasticsearch 23 | ``` 24 | 25 | ## Configuration 26 | Before the Elasticsearch module can display anything, you have to configure at least one instance and one event type. 27 | Read the [Configuration](03-Configuration.md) section for details. -------------------------------------------------------------------------------- /doc/03-Configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | This chapter will give you the very basics to get the Elasticsearch module for Icinga Web 2 up and running. 4 | 5 | ## Elasticsearch Instances 6 | 7 | The first step to take here is to define how to connect to your Elasticsearch instances. Using the web interface is 8 | the preferred method of configuration. Please access `Configuration -> Modules -> elasticsearch -> Instances` in order 9 | to set up a new Elasticsearch instance. 10 | 11 | ![Configuration New Instance](res/screenshots/02-Configuration-New-Instance.png) 12 | 13 | 14 | | Option | Required | Description | 15 | | --------------------- | -------- | ----------------------------------- | 16 | | Name | **yes** | Name of the Elasticsearch instance. | 17 | | URI | **yes** | URI of the Elasticsearch instance. | 18 | | User | no | Username | 19 | | Password | no | Password | 20 | | Certificate Authority | no | The path of the file containing one or more certificates to verify the peer with or the path to the directory that holds multiple CA certificates. | 21 | | Client Certificate | no | The path of the client certificate. | 22 | | Client Private Key | no | The path of the client private key. | 23 | 24 | ## Event Types 25 | 26 | Event types define how to access data in your Elasticsearch instances. Again, please use the web interface for 27 | configuration and access `Configuration -> Modules -> elasticsearch -> Event Types`. 28 | 29 | ![Configuration New Event Type](res/screenshots/02-Configuration-New-Event-Type.png) 30 | 31 | | Option | Required | Description | 32 | | --------------------- | -------- | ----------------------------------------------- | 33 | | Name | **yes** | Name of the event type. | 34 | | Instance | **yes** | Elasticsearch instance to connect to. | 35 | | Index | **yes** | Elasticsearch index pattern, e.g. `filebeat-*`. | 36 | | Filter | **yes** | Elasticsearch filter in the Icinga Web 2 URL filter format. Host macros are evaluated if you encapsulate them in curly braces, e.g. `host={host.name}&location={_host_location}`. | 37 | | Fields | **yes** | Comma-separated list of field names to display. One or more wildcard asterisk (`*`) patterns are also accepted. Note that the `@timestamp` field is always respected. | 38 | 39 | ### Examples 40 | 41 | Some examples that may help you to create your own Event Types. You can either use the webinterface or copy the 42 | configuration directly into `/etc/icingaweb2/modules/elasticsearch/eventtypes.ini`. 43 | 44 | #### Filebeat 45 | 46 | ```ini 47 | [Filebeat] 48 | instance = "Elasticsearch" 49 | index = "filebeat-*" 50 | filter = "beat.hostname={host.name}" 51 | fields = "input_type, source, message" 52 | ``` 53 | 54 | #### Logstash with Syslog Filter 55 | This Logstash example is based on the configuration examples of the [Logstash documentation](https://www.elastic.co/guide/en/logstash/current/config-examples.html). 56 | 57 | ```ini 58 | [Logstash] 59 | instance = "Elasticsearch" 60 | index = "logstash-*" 61 | filter = "syslog_hostname={host.name}&type=syslog" 62 | fields = "syslog_timestamp, syslog_program, syslog_message" 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /doc/04-Checks.md: -------------------------------------------------------------------------------- 1 | # Icinga 2 Checks 2 | 3 | This module brings an `icingacli` command to check if certain events are present in Elasticsearch. 4 | 5 | ## Example 6 | 7 | The following will check if there are more than 3 (warning) or 5 (critical) events of severity `critical` from the host `www.example.com` in the data from the last hour. 8 | 9 | * The `instance` is the same which was set in the modules configuration 10 | * The values of `crit` and `warn` are just numerical thresholds 11 | * `index` is set to an index pattern in Elasticsearch. It's a pattern that has to match all index names to search 12 | * As a `filter` the check takes a filter in Icinga Web 2's filter syntax. These are comparisons of fields in Elasticsearch to values 13 | 14 | ``` 15 | # icingacli elasticsearch check --instance elasticsearch --crit 5 --warn 3 --index logstash* --filter "beat.hostname=www.example.com AND severity=critical" --from -1h 16 | ``` 17 | -------------------------------------------------------------------------------- /doc/res/screenshots/02-Configuration-New-Event-Type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-elasticsearch/2dc602bc79f2bd91d5bc7ccac780cc7532601a92/doc/res/screenshots/02-Configuration-New-Event-Type.png -------------------------------------------------------------------------------- /doc/res/screenshots/02-Configuration-New-Instance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-elasticsearch/2dc602bc79f2bd91d5bc7ccac780cc7532601a92/doc/res/screenshots/02-Configuration-New-Instance.png -------------------------------------------------------------------------------- /doc/res/screenshots/99-Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Icinga/icingaweb2-module-elasticsearch/2dc602bc79f2bd91d5bc7ccac780cc7532601a92/doc/res/screenshots/99-Overview.png -------------------------------------------------------------------------------- /examples/event-types.ini: -------------------------------------------------------------------------------- 1 | ; Examples for Syslog event types 2 | [syslog] 3 | label = "Syslog Other" 4 | description = "" 5 | filter = "type=syslog&facility_label!=system&facility_label!=security*&facility_label!=kernel&facility_label!=user-level" 6 | fields = "logsource,facility_label,program,severity_label,message" 7 | name = "syslog" 8 | hostmap_filter = "logsource=${host_name}" 9 | 10 | [syslog-kernel] 11 | label = "Syslog System" 12 | description = "" 13 | filter = "type=syslog&(facility_label=kernel|facility_label=system)" 14 | fields = "logsource,facility_label,program,severity_label,message" 15 | name = "syslog-kernel" 16 | hostmap_filter = "logsource=${host_name}" 17 | 18 | [syslog-security] 19 | label = "Syslog Security" 20 | description = "Security events" 21 | filter = "type=syslog&facility_label=security*" 22 | fields = "logsource,facility_label,program,severity_label,message" 23 | name = "syslog-security" 24 | hostmap_filter = "logsource=${host_name}" 25 | 26 | [syslog-user] 27 | label = "Syslog User" 28 | description = "" 29 | filter = "type=syslog&facility_label=user-level" 30 | fields = "logsource,program,severity_label,message" 31 | name = "syslog-user" 32 | hostmap_filter = "logsource=${host_name}" 33 | -------------------------------------------------------------------------------- /library/Elasticsearch/AutorefreshControlWidget.php: -------------------------------------------------------------------------------- 1 | interval; 29 | } 30 | 31 | /** 32 | * Set the auto-refresh interval 33 | * 34 | * @param int $interval 35 | * 36 | * @return $this 37 | */ 38 | public function setInterval($interval) 39 | { 40 | $this->interval = $interval; 41 | 42 | return $this; 43 | } 44 | 45 | public function render() 46 | { 47 | $control = new AutorefreshControlForm(); 48 | $control 49 | ->setInterval($this->getInterval()) 50 | ->handleRequest(); 51 | 52 | return (string) $control; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/Elasticsearch/Controller.php: -------------------------------------------------------------------------------- 1 | view->compact) { 23 | $this->view->autorefreshControl = $widget; 24 | } 25 | 26 | $interval = (int) $this->getRequest()->getParam('refresh', $interval); 27 | 28 | if ($interval !== null && $interval > 0) { 29 | $widget->setInterval($interval); 30 | $this->setAutorefreshInterval($interval); 31 | } 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * Set the title tab 38 | * 39 | * @param string $label 40 | */ 41 | public function setTitle($label) 42 | { 43 | $this->getTabs()->add(uniqid(), [ 44 | 'active' => true, 45 | 'label' => $label, 46 | 'url' => $this->getRequest()->getUrl() 47 | ]); 48 | } 49 | 50 | public function paginate(Paginatable $paginatable, $itemsPerPage = 25, $pageNumber = 0) 51 | { 52 | $request = $this->getRequest(); 53 | $limit = $request->getParam('limit', $itemsPerPage); 54 | $page = $request->getParam('page', $pageNumber); 55 | $paginatable->limit($limit, $page > 0 ? ($page - 1) * $limit : 0); 56 | 57 | if (! $this->view->compact) { 58 | $paginator = new Paginator(); 59 | $paginator->setQuery($paginatable); 60 | $this->view->paginator = $paginator; 61 | } 62 | 63 | return $this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /library/Elasticsearch/Elastic.php: -------------------------------------------------------------------------------- 1 | config = $config; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | * 22 | * @return Query 23 | */ 24 | public function select(array $columns = []) 25 | { 26 | return new Query($this, $columns); 27 | } 28 | 29 | /** 30 | * @return object 31 | */ 32 | public function getConfig() 33 | { 34 | return $this->config; 35 | } 36 | 37 | public static function extractFields($source, &$fields, array $parent = []) 38 | { 39 | foreach ($source as $key => $value) { 40 | if ($key === '@timestamp') { 41 | $fields['@timestamp'] = function($event) { 42 | $value = new \DateTime($event['@timestamp'], new \DateTimeZone('UTC')); 43 | $value = $value->setTimezone(new \DateTimeZone(date_default_timezone_get())); 44 | $value = $value->format('Y-m-d H:i:s'); 45 | 46 | return $value; 47 | }; 48 | continue; 49 | } 50 | if (is_array($value)) { 51 | static::extractFields($value, $fields, array_merge($parent, [$key])); 52 | } else { 53 | $field = array_merge($parent, [$key]); 54 | $fields[implode('_', $field)] = call_user_func(function($field) { 55 | return function($event) use ($field) { 56 | if (empty ($field)) { 57 | return null; 58 | } 59 | 60 | $value = $event; 61 | 62 | foreach ($field as $key) { 63 | if (! isset($value[$key])) { 64 | return null; 65 | } 66 | 67 | $value = $value[$key]; 68 | } 69 | 70 | return $value; 71 | }; 72 | }, $field); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /library/Elasticsearch/Eventtypes.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'eventtypes', 13 | 'keyColumn' => 'name', 14 | 'module' => 'elasticsearch' 15 | ] 16 | ]; 17 | 18 | protected $queryColumns = [ 19 | 'eventtypes' => [ 20 | 'name', 21 | 'instance', 22 | 'index', 23 | 'filter', 24 | 'fields' 25 | ] 26 | ]; 27 | } 28 | -------------------------------------------------------------------------------- /library/Elasticsearch/Exception/RestApiException.php: -------------------------------------------------------------------------------- 1 | errorCode = (int) $errorCode; 27 | return $this; 28 | } 29 | 30 | /** 31 | * Return the curl error code 32 | * 33 | * @return int 34 | */ 35 | public function getErrorCode() 36 | { 37 | return $this->code; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /library/Elasticsearch/FilterRenderer.php: -------------------------------------------------------------------------------- 1 | setFilter($filter); 42 | } 43 | } 44 | 45 | /** 46 | * Set the filter and render it internally. 47 | * 48 | * @param Filter $filter 49 | * 50 | * @return $this 51 | * 52 | * @throws ProgrammingError 53 | */ 54 | public function setFilter(Filter $filter) 55 | { 56 | $this->filter = $filter; 57 | $this->query = $this->renderFilter($this->filter); 58 | Logger::debug('Rendered elasticsearch filter: %s', json_encode($this->query)); 59 | return $this; 60 | } 61 | 62 | /** 63 | * Returns the rendered filter as an Array for the REST API. 64 | * 65 | * @return array 66 | */ 67 | public function getQuery() 68 | { 69 | return $this->query; 70 | } 71 | 72 | /** 73 | * Renders the Elasticsearch query as a recursive function, walking through the FilterChain. 74 | * 75 | * @param Filter $filter Current object 76 | * @param int $level Current depth in integer 77 | * 78 | * @return array|null|string 79 | * 80 | * @throws ProgrammingError 81 | */ 82 | protected function renderFilter(Filter $filter, $level = 0) 83 | { 84 | $container = array(); 85 | if ($filter->isChain()) { 86 | if ($filter instanceof FilterAnd) { 87 | $section = 'must'; 88 | } elseif ($filter instanceof FilterOr) { 89 | $section = 'should'; 90 | } elseif ($filter instanceof FilterNot) { 91 | $section = 'must_not'; 92 | } else { 93 | throw new ProgrammingError('Cannot render filter chain type: %s', get_class($filter)); 94 | } 95 | 96 | if (! $filter->isEmpty()) { 97 | /** @var Filter $filterPart */ 98 | foreach ($filter->filters() as $filterPart) { 99 | $part = $this->renderFilter($filterPart, $level + 1); 100 | if ($part) { 101 | if ($filter instanceof FilterNot) { 102 | // add in a new bool to flip expression 103 | if ($filterPart instanceof FilterMatchNot) { 104 | $container[$section][] = array( 105 | 'bool' => array( 106 | 'must_not' => $part, 107 | ), 108 | ); 109 | continue; 110 | } 111 | } elseif ($filter instanceof FilterAnd) { 112 | // add match not to must_not instead of must 113 | if ($filterPart instanceof FilterMatchNot) { 114 | $container['must_not'][] = $part; 115 | continue; 116 | } 117 | // merge in must_not 118 | elseif ($filterPart instanceof FilterNot) { 119 | $container['must_not'] = $part['bool']['must_not']; 120 | continue; 121 | } 122 | } 123 | $container[$section][] = $part; 124 | } 125 | } 126 | // return the bool of the chain 127 | return array('bool' => $container); 128 | } else { 129 | // return match_all 130 | return array( 131 | 'match_all' => (object) array(), 132 | ); 133 | } 134 | } else { 135 | // return the simple part 136 | return $this->renderFilterExpression($filter); 137 | } 138 | } 139 | 140 | /** 141 | * Render and return the given filter expression. 142 | * 143 | * This handles non-chain parts of the Filter. 144 | * 145 | * @param Filter $filter 146 | * @return string 147 | * @throws ProgrammingError 148 | */ 149 | protected function renderFilterExpression(Filter $filter) 150 | { 151 | /** @var FilterMatch $filter (just for resolving) */ 152 | $column = $filter->getColumn(); 153 | $sign = $filter->getSign(); 154 | $value = $filter->getExpression(); 155 | 156 | // array or lists 157 | if (is_array($value)) { 158 | if ($sign === '=' || $sign === '!=') { 159 | return array( 160 | 'query_string' => array( 161 | 'default_field' => $column, 162 | 'query' => '"' . join('" "', $value) . '"' 163 | ) 164 | ); 165 | } 166 | 167 | throw new ProgrammingError( 168 | 'Unable to render array expressions with operators other than equal or not equal' 169 | ); 170 | } 171 | // with wildcards 172 | elseif (strpos($value, '*') !== false) { 173 | if ($value === '*') { 174 | // (sign =) We'll ignore such filters as it prevents index usage and because "*" means anything, anything means 175 | // all whereas all means that whether we use a filter to match anything or no filter at all makes no 176 | // difference, except for performance reasons... 177 | 178 | // (sign !=) We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're 179 | // using a real column with a valid comparison here or just an expression which cannot be evaluated to 180 | // true makes no difference, except for performance reasons... 181 | return null; 182 | } 183 | 184 | if ($sign === '=' || $sign === '!=') { 185 | return array( 186 | 'query_string' => array( 187 | 'default_field' => $column, 188 | 'query' => $value, 189 | 'analyze_wildcard' => true 190 | ) 191 | ); 192 | 193 | } 194 | 195 | throw new ProgrammingError( 196 | 'Unable to render expressions wildcards other than equal or not equal' 197 | ); 198 | } 199 | // any other value 200 | else { 201 | // simple comparison via match 202 | if ($sign === '=' || $sign === '!=') { 203 | return array( 204 | 'match_phrase' => array( 205 | $column => $value, 206 | ), 207 | ); 208 | } elseif (preg_match('/^[<>]=?$/', $sign)) { 209 | $param_map = array( 210 | '>' => 'gt', 211 | '<' => 'lt', 212 | '>=' => 'gte', 213 | '<=' => 'lte', 214 | ); 215 | return array( 216 | 'range' => array( 217 | $column => array( 218 | $param_map[$sign] => $value, 219 | ), 220 | ), 221 | ); 222 | } 223 | 224 | throw new ProgrammingError( 225 | 'Unable to render string expressions with operators other than equality and range' 226 | ); 227 | } 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /library/Elasticsearch/Instances.php: -------------------------------------------------------------------------------- 1 | [ 12 | 'name' => 'instances', 13 | 'keyColumn' => 'name', 14 | 'module' => 'elasticsearch' 15 | ] 16 | ]; 17 | 18 | protected $queryColumns = [ 19 | 'instances' => [ 20 | 'name', 21 | 'uri', 22 | 'user', 23 | 'password', 24 | 'ca', 25 | 'client_certificate', 26 | 'client_private_key' 27 | ] 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /library/Elasticsearch/ProvidedHook/Monitoring/HostActions.php: -------------------------------------------------------------------------------- 1 | createNavigation([ 15 | mt('elasticsearch', 'Elasticsearch Events') => [ 16 | 'icon' => 'doc-text', 17 | 'permission' => 'elasticsearch/events', 18 | 'url' => Url::fromPath('elasticsearch/events', ['host' => $host->getName()]) 19 | ] 20 | ]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /library/Elasticsearch/Query.php: -------------------------------------------------------------------------------- 1 | elastic = $elastic; 37 | 38 | $this->fields = $fields; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | * 44 | * @return $this 45 | */ 46 | public function from($target, array $fields = null) 47 | { 48 | $this->index = $target; 49 | 50 | if (! empty($fields)) { 51 | $this->fields = $fields; 52 | } 53 | 54 | return $this; 55 | } 56 | 57 | public function limit($count = null, $offset = null) 58 | { 59 | $this->limit = $count; 60 | $this->offset = $offset; 61 | 62 | return $this; 63 | } 64 | 65 | public function hasLimit() 66 | { 67 | return $this->limit !== null; 68 | } 69 | 70 | public function getLimit() 71 | { 72 | return $this->limit; 73 | } 74 | 75 | public function hasOffset() 76 | { 77 | return $this->offset !== null; 78 | } 79 | 80 | public function getOffset() 81 | { 82 | return $this->offset; 83 | } 84 | 85 | public function count() 86 | { 87 | $this->execute(); 88 | 89 | $total = $this->response['hits']['total']; 90 | if ($total > self::MAX_RESULT_WINDOW) { 91 | return self::MAX_RESULT_WINDOW; 92 | } 93 | 94 | return $total; 95 | } 96 | 97 | public function filter($filter) 98 | { 99 | $this->filter = $filter; 100 | 101 | return $this; 102 | } 103 | 104 | protected function execute() 105 | { 106 | if ($this->response === null) { 107 | $config = $this->elastic->getConfig(); 108 | 109 | $client = new Client(); 110 | 111 | $curl = []; 112 | 113 | if (! empty($config->ca)) { 114 | if (is_dir($config->ca) 115 | || (is_link($config->ca) && is_dir(readlink($config->ca))) 116 | ) { 117 | $curl[CURLOPT_CAPATH] = $config->ca; 118 | } else { 119 | $curl[CURLOPT_CAINFO] = $config->ca; 120 | } 121 | } 122 | 123 | if (! empty($config->client_certificate)) { 124 | $curl[CURLOPT_SSLCERT] = $config->client_certificate; 125 | } 126 | 127 | if (! empty($config->client_private_key)) { 128 | $curl[CURLOPT_SSLCERT] = $config->client_private_key; 129 | } 130 | 131 | $uri = (new Uri("{$config->uri}/{$this->index}/_search")) 132 | ->withUserInfo($config->user, $config->password); 133 | 134 | $request = new Request( 135 | 'GET', 136 | $uri, 137 | ['Content-Type' => 'application/json'], 138 | json_encode(array_filter(array_merge([ 139 | '_source' => array_merge(['@timestamp'], $this->fields), 140 | 'query' => $this->filter, 141 | 'from' => $this->getOffset(), 142 | 'size' => $this->getLimit(), 143 | 'sort' => ['@timestamp' => 'desc'] 144 | ], $this->patch), function ($part) { return $part !== null; })) 145 | ); 146 | 147 | $response = Json::decode((string) $client->send($request, ['curl' => $curl])->getBody(), true); 148 | 149 | if (isset($response['error'])) { 150 | throw new RuntimeException( 151 | 'Got error from Elasticsearch: '. $response['error']['type'] . ': ' . $response['error']['reason'] 152 | ); 153 | } 154 | 155 | $this->response = $response; 156 | } 157 | } 158 | 159 | public function getFields() 160 | { 161 | $this->execute(); 162 | 163 | $events = $this->response['hits']['hits']; 164 | 165 | $fields = []; 166 | 167 | if (! empty($events)) { 168 | $event = reset($events); 169 | 170 | Elastic::extractFields($event['_source'], $fields); 171 | } 172 | 173 | // Sort random order fields received from ES based on the configured fields order 174 | $sort = array_map(function ($field) { 175 | return str_replace('.', '_', $field); 176 | }, $this->fields); 177 | 178 | array_unshift($sort, '@timestamp'); 179 | 180 | $sort = array_flip($sort); 181 | 182 | $fields = array_replace($sort, $fields); 183 | 184 | return $fields; 185 | } 186 | 187 | public function fetchAll() 188 | { 189 | $this->execute(); 190 | 191 | return $this->response['hits']['hits']; 192 | } 193 | 194 | public function patch(array $patch) 195 | { 196 | $this->patch = $patch; 197 | 198 | return $this; 199 | } 200 | 201 | public function getResponse() 202 | { 203 | $this->execute(); 204 | 205 | return $this->response; 206 | } 207 | 208 | public function get($target) 209 | { 210 | $config = $this->elastic->getConfig(); 211 | 212 | $client = new Client(); 213 | 214 | $curl = []; 215 | 216 | if (! empty($config->ca)) { 217 | if (is_dir($config->ca) 218 | || (is_link($config->ca) && is_dir(readlink($config->ca))) 219 | ) { 220 | $curl[CURLOPT_CAPATH] = $config->ca; 221 | } else { 222 | $curl[CURLOPT_CAINFO] = $config->ca; 223 | } 224 | } 225 | 226 | if (! empty($config->client_certificate)) { 227 | $curl[CURLOPT_SSLCERT] = $config->client_certificate; 228 | } 229 | 230 | if (! empty($config->client_private_key)) { 231 | $curl[CURLOPT_SSLCERT] = $config->client_private_key; 232 | } 233 | 234 | $uri = (new Uri("{$config->uri}/{$target}")) 235 | ->withUserInfo($config->user, $config->password); 236 | 237 | $request = new Request( 238 | 'GET', 239 | $uri, 240 | ['Content-Type' => 'application/json'] 241 | ); 242 | 243 | $response = Json::decode((string) $client->send($request, ['curl' => $curl])->getBody(), true); 244 | 245 | if (isset($response['error'])) { 246 | throw new RuntimeException( 247 | 'Got error from Elasticsearch: '. $response['error']['type'] . ': ' . $response['error']['reason'] 248 | ); 249 | } 250 | 251 | return $response; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/CountApiRequest.php: -------------------------------------------------------------------------------- 1 | id === null) { 21 | throw new LogicException('DeleteApiRequest is missing a document id'); 22 | } 23 | 24 | return sprintf('/%s/%s/%s', $this->index, $this->documentType, $this->id); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/DocumentApiRequest.php: -------------------------------------------------------------------------------- 1 | id = $id; 40 | $this->index = $index; 41 | $this->documentType = $documentType; 42 | $this->setPayload($data, $data !== null ? 'application/json' : null); 43 | } 44 | 45 | /** 46 | * Create and return the path for this DocumentApiRequest 47 | * 48 | * @return string 49 | */ 50 | abstract protected function createPath(); 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getPath() 56 | { 57 | if ($this->path === null) { 58 | $this->path = $this->createPath(); 59 | } 60 | 61 | return $this->path; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getPayload() 68 | { 69 | $payload = parent::getPayload(); 70 | if ($payload !== null) { 71 | $payload = $this->jsonEncode($payload); 72 | } 73 | 74 | return $payload; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/FilterRenderer.php: -------------------------------------------------------------------------------- 1 | setFilter($filter); 42 | } 43 | } 44 | 45 | /** 46 | * Set the filter and render it internally. 47 | * 48 | * @param Filter $filter 49 | * 50 | * @return $this 51 | * 52 | * @throws ProgrammingError 53 | */ 54 | public function setFilter(Filter $filter) 55 | { 56 | $this->filter = $filter; 57 | $this->query = $this->renderFilter($this->filter); 58 | Logger::debug('Rendered elasticsearch filter: %s', json_encode($this->query)); 59 | return $this; 60 | } 61 | 62 | /** 63 | * Returns the rendered filter as an Array for the REST API. 64 | * 65 | * @return array 66 | */ 67 | public function getQuery() 68 | { 69 | return $this->query; 70 | } 71 | 72 | /** 73 | * Renders the Elasticsearch query as a recursive function, walking through the FilterChain. 74 | * 75 | * @param Filter $filter Current object 76 | * @param int $level Current depth in integer 77 | * 78 | * @return array|null|string 79 | * 80 | * @throws ProgrammingError 81 | */ 82 | protected function renderFilter(Filter $filter, $level = 0) 83 | { 84 | $container = array(); 85 | if ($filter->isChain()) { 86 | if ($filter instanceof FilterAnd) { 87 | $section = 'must'; 88 | } elseif ($filter instanceof FilterOr) { 89 | $section = 'should'; 90 | } elseif ($filter instanceof FilterNot) { 91 | $section = 'must_not'; 92 | } else { 93 | throw new ProgrammingError('Cannot render filter chain type: %s', get_class($filter)); 94 | } 95 | 96 | if (! $filter->isEmpty()) { 97 | /** @var Filter $filterPart */ 98 | foreach ($filter->filters() as $filterPart) { 99 | $part = $this->renderFilter($filterPart, $level + 1); 100 | if ($part) { 101 | if ($filter instanceof FilterNot) { 102 | // add in a new bool to flip expression 103 | if ($filterPart instanceof FilterMatchNot) { 104 | $container[$section][] = array( 105 | 'bool' => array( 106 | 'must_not' => $part, 107 | ), 108 | ); 109 | continue; 110 | } 111 | } elseif ($filter instanceof FilterAnd) { 112 | // add match not to must_not instead of must 113 | if ($filterPart instanceof FilterMatchNot) { 114 | $container['must_not'][] = $part; 115 | continue; 116 | } 117 | // merge in must_not 118 | elseif ($filterPart instanceof FilterNot) { 119 | $container['must_not'] = $part['bool']['must_not']; 120 | continue; 121 | } 122 | } 123 | $container[$section][] = $part; 124 | } 125 | } 126 | // return the bool of the chain 127 | return array('bool' => $container); 128 | } else { 129 | // return match_all 130 | return array( 131 | 'match_all' => (object) array(), 132 | ); 133 | } 134 | } else { 135 | // return the simple part 136 | return $this->renderFilterExpression($filter); 137 | } 138 | } 139 | 140 | /** 141 | * Render and return the given filter expression. 142 | * 143 | * This handles non-chain parts of the Filter. 144 | * 145 | * @param Filter $filter 146 | * @return string 147 | * @throws ProgrammingError 148 | */ 149 | protected function renderFilterExpression(Filter $filter) 150 | { 151 | /** @var FilterMatch $filter (just for resolving) */ 152 | $column = $filter->getColumn(); 153 | $sign = $filter->getSign(); 154 | $value = $filter->getExpression(); 155 | 156 | // array or lists 157 | if (is_array($value)) { 158 | if ($sign === '=' || $sign === '!=') { 159 | return array( 160 | 'query_string' => array( 161 | 'default_field' => $column, 162 | 'query' => '"' . join('" "', $value) . '"' 163 | ) 164 | ); 165 | } 166 | 167 | throw new ProgrammingError( 168 | 'Unable to render array expressions with operators other than equal or not equal' 169 | ); 170 | } 171 | // with wildcards 172 | elseif (strpos($value, '*') !== false) { 173 | if ($value === '*') { 174 | // (sign =) We'll ignore such filters as it prevents index usage and because "*" means anything, anything means 175 | // all whereas all means that whether we use a filter to match anything or no filter at all makes no 176 | // difference, except for performance reasons... 177 | 178 | // (sign !=) We'll ignore such filters as it prevents index usage and because "*" means nothing, so whether we're 179 | // using a real column with a valid comparison here or just an expression which cannot be evaluated to 180 | // true makes no difference, except for performance reasons... 181 | return null; 182 | } 183 | 184 | if ($sign === '=' || $sign === '!=') { 185 | return array( 186 | 'query_string' => array( 187 | 'default_field' => $column, 188 | 'query' => $value, 189 | 'analyze_wildcard' => true 190 | ) 191 | ); 192 | 193 | } 194 | 195 | throw new ProgrammingError( 196 | 'Unable to render expressions wildcards other than equal or not equal' 197 | ); 198 | } 199 | // any other value 200 | else { 201 | // simple comparison via match 202 | if ($sign === '=' || $sign === '!=') { 203 | return array( 204 | 'match' => array( 205 | $column => $value, 206 | ), 207 | ); 208 | } elseif (preg_match('/^[<>]=?$/', $sign)) { 209 | $param_map = array( 210 | '>' => 'gt', 211 | '<' => 'lt', 212 | '>=' => 'gte', 213 | '<=' => 'lte', 214 | ); 215 | return array( 216 | 'range' => array( 217 | $column => array( 218 | $param_map[$sign] => $value, 219 | ), 220 | ), 221 | ); 222 | } 223 | 224 | throw new ProgrammingError( 225 | 'Unable to render string expressions with operators other than equality and range' 226 | ); 227 | } 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/GetApiRequest.php: -------------------------------------------------------------------------------- 1 | sourceOnly = (bool) $state; 32 | return $this; 33 | } 34 | 35 | /** 36 | * Return whether to only fetch the source 37 | * 38 | * @return bool 39 | */ 40 | public function getSourceOnly() 41 | { 42 | return $this->sourceOnly ?: false; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function createPath() 49 | { 50 | if ($this->id === null) { 51 | throw new LogicException('GetApiRequest is missing a document id'); 52 | } 53 | 54 | $path = sprintf('/%s/%s/%s', $this->index, $this->documentType, $this->id); 55 | if ($this->getSourceOnly()) { 56 | $path .= '/_source'; 57 | } 58 | 59 | return $path; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/GetIndicesApiRequest.php: -------------------------------------------------------------------------------- 1 | indices = $indices; 36 | $this->settings = $settings; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getPath() 43 | { 44 | if ($this->path === null) { 45 | $this->path = sprintf('/%s', join(',', $this->indices)); 46 | if (! empty($this->settings)) { 47 | $this->path .= '/' . join(',', $this->settings); 48 | } 49 | } 50 | 51 | return $this->path; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/GetMappingApiRequest.php: -------------------------------------------------------------------------------- 1 | id === null) { 31 | return sprintf('/%s/%s', $this->index, $this->documentType); 32 | } 33 | 34 | return sprintf('/%s/%s/%s/_create', $this->index, $this->documentType, $this->id); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/MappingApiRequest.php: -------------------------------------------------------------------------------- 1 | indices = $indices; 40 | $this->types = $types; 41 | $this->fields = $fields; 42 | $this->setPayload($data, $data !== null ? 'application/json' : null); 43 | } 44 | 45 | /** 46 | * Create and return the path for this MappingApiRequest 47 | * 48 | * @return string 49 | */ 50 | protected function createPath() 51 | { 52 | if (! empty($this->indices)) { 53 | $path = sprintf('/%s/_mappings', join(',', $this->indices)); 54 | } else { 55 | $path = '/_mappings'; 56 | } 57 | 58 | if (! empty($this->types)) { 59 | $path .= '/' . join(',', $this->types); 60 | } 61 | 62 | if (! empty($this->indices) && !empty($this->fields)) { 63 | $path .= '/field/' . join(',', $this->fields); 64 | } 65 | 66 | return $path; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function getPath() 73 | { 74 | if ($this->path === null) { 75 | $this->path = $this->createPath(); 76 | } 77 | 78 | return $this->path; 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | public function getPayload() 85 | { 86 | $payload = parent::getPayload(); 87 | if ($payload !== null) { 88 | $payload = $this->jsonEncode($payload); 89 | } 90 | 91 | return $payload; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/RestApiQuery.php: -------------------------------------------------------------------------------- 1 | indices = $indices; 67 | return $this; 68 | } 69 | 70 | /** 71 | * Return the patterns defining the indices where to search for documents 72 | * 73 | * @return array|null 74 | */ 75 | public function getIndices() 76 | { 77 | return $this->indices; 78 | } 79 | 80 | /** 81 | * Set the names of the document types to search for 82 | * 83 | * @param array $types 84 | * 85 | * @return $this 86 | */ 87 | public function setTypes(array $types = null) 88 | { 89 | $this->types = $types; 90 | return $this; 91 | } 92 | 93 | /** 94 | * Return the names of the document types to search for 95 | * 96 | * @return array|null 97 | */ 98 | public function getTypes() 99 | { 100 | return $this->types; 101 | } 102 | 103 | /** 104 | * Set the URL params to use for count and search requests 105 | * 106 | * @param UrlParams $params 107 | * 108 | * @return $this 109 | */ 110 | public function setParams(UrlParams $params) 111 | { 112 | $this->params = $params; 113 | return $this; 114 | } 115 | 116 | /** 117 | * Return the URL params to use for count and search requests 118 | * 119 | * @return UrlParams 120 | */ 121 | public function getParams() 122 | { 123 | if ($this->params === null) { 124 | $this->params = new UrlParams(); 125 | } 126 | 127 | return $this->params; 128 | } 129 | 130 | /** 131 | * Set whether _source retrieval is disabled 132 | * 133 | * @param bool $state 134 | * 135 | * @return $this 136 | */ 137 | public function disableSourceRetrieval($state) 138 | { 139 | $this->disabledSource = (bool) $state; 140 | return $this; 141 | } 142 | 143 | /** 144 | * Return whether _source retrieval is disabled 145 | * 146 | * @return bool 147 | */ 148 | public function isSourceRetrievalDisabled() 149 | { 150 | return $this->disabledSource ?: false; 151 | } 152 | 153 | /** 154 | * Set the field to be used to unfold the result 155 | * 156 | * @param string $field 157 | * 158 | * @return $this 159 | */ 160 | public function setUnfoldAttribute($field) 161 | { 162 | $this->unfoldAttribute = $field; 163 | return $this; 164 | } 165 | 166 | /** 167 | * Return the field to use to unfold the result 168 | * 169 | * @return string 170 | */ 171 | public function getUnfoldAttribute() 172 | { 173 | return $this->unfoldAttribute; 174 | } 175 | 176 | /** 177 | * Choose a document type and the fields you are interested in 178 | * 179 | * {@inheritdoc} This registers the given target as type filter. 180 | */ 181 | public function from($target, array $fields = null) 182 | { 183 | $this->setTypes(array($target)); 184 | return parent::from($target, $fields); 185 | } 186 | 187 | /** 188 | * Create and return a new instance of CountApiRequest for this query 189 | * 190 | * @return CountApiRequest 191 | */ 192 | public function createCountRequest() 193 | { 194 | $foldedColumn = $this->getUnfoldAttribute(); 195 | if ($foldedColumn === null) { 196 | $request = new CountApiRequest( 197 | $this->getIndices(), 198 | $this->getTypes(), 199 | array('query' => $this->ds->renderFilter($this->getFilter())) 200 | ); 201 | return $request->setParams($this->getParams()); 202 | } 203 | 204 | $requestedFields = $this->getColumns(); 205 | if (isset($requestedFields[$foldedColumn])) { 206 | $foldedField = $requestedFields[$foldedColumn]; 207 | } elseif (in_array($foldedColumn, $requestedFields, true)) { 208 | $foldedField = $foldedColumn; 209 | } else { 210 | throw new LogicException('The field used to unfold a query\'s result must be selected'); 211 | } 212 | 213 | $request = new SearchApiRequest( 214 | $this->getIndices(), 215 | $this->getTypes(), 216 | array( 217 | 'query' => $this->ds->renderFilter($this->getFilter()), 218 | 'aggs' => array( 219 | 'unfolded_count' => array( 220 | 'value_count' => array( 221 | 'field' => $foldedField 222 | ) 223 | ) 224 | ) 225 | ) 226 | ); 227 | 228 | $params = $this->getParams() 229 | ->set('_source', 'false') 230 | ->set('filter_path', 'aggregations'); 231 | return $request->setParams($params); 232 | } 233 | 234 | /** 235 | * Create and return the result for the given count response 236 | * 237 | * @param RestApiResponse $response 238 | * 239 | * @return int 240 | */ 241 | public function createCountResult(RestApiResponse $response) 242 | { 243 | $json = $response->json(); 244 | if ($this->getUnfoldAttribute() === null) { 245 | return $json['count']; 246 | } else { 247 | return $json['aggregations']['unfolded_count']['value']; 248 | } 249 | } 250 | 251 | /** 252 | * Create and return a new instance of SearchApiRequest for this query 253 | * 254 | * @return SearchApiRequest 255 | */ 256 | public function createSearchRequest() 257 | { 258 | $body = array( 259 | 'from' => $this->getOffset() ?: static::DEFAULT_OFFSET, 260 | 'size' => $this->hasLimit() ? $this->getLimit() : static::DEFAULT_LIMIT, 261 | 'query' => $this->ds->renderFilter($this->getFilter()) 262 | ); 263 | if ($this->hasOrder()) { 264 | $sort = array(); 265 | foreach ($this->getOrder() as $order) { 266 | $sort[] = array($order[0] => strtolower($order[1])); 267 | } 268 | 269 | $body['sort'] = $sort; 270 | } 271 | 272 | $fields = $this->getColumns(); 273 | if ($this->isSourceRetrievalDisabled()) { 274 | $body['_source'] = false; 275 | } elseif (! empty($fields)) { 276 | $sourceFields = array(); 277 | foreach ($fields as $fieldName) { 278 | if (substr($fieldName, 0, 1) !== '_') { 279 | $sourceFields[] = $fieldName; 280 | } 281 | } 282 | 283 | $body['_source'] = empty($sourceFields) ? false : $sourceFields; 284 | } 285 | 286 | if ($this->getUnfoldAttribute() !== null) { 287 | foreach ($this->foldedHighlights() as $field) { 288 | $body['highlight']['fields'][$field] = array( 289 | 'pre_tags' => array(''), 290 | 'post_tags' => array(''), 291 | 'require_field_match' => true, 292 | 'number_of_fragments' => 0 293 | ); 294 | } 295 | 296 | $body['from'] = 0; 297 | if ($this->hasOffset() && $this->hasLimit()) { 298 | $limit = ($this->limitOffset / $this->limitCount + 1) * $this->limitCount; 299 | if ($this->peekAhead) { 300 | $limit += 1; 301 | } 302 | 303 | $body['size'] = $limit; 304 | } 305 | } 306 | 307 | $request = new SearchApiRequest($this->getIndices(), $this->getTypes(), $body); 308 | return $request->setParams($this->getParams()); 309 | } 310 | 311 | /** 312 | * Create and return a result set for the given search response 313 | * 314 | * @param RestApiResponse $response 315 | * 316 | * @return array 317 | */ 318 | public function createSearchResult(RestApiResponse $response) 319 | { 320 | $foldedColumn = $this->getUnfoldAttribute(); 321 | $requestedFields = $this->getColumns(); 322 | $offset = $this->getOffset(); 323 | $limit = $this->getLimit(); 324 | 325 | $count = 0; 326 | $result = array(); 327 | $json = $response->json(); 328 | foreach ($json['hits']['hits'] as $hit) { 329 | $hit = new SearchHit($hit); 330 | if ($foldedColumn === null) { 331 | $result[] = $hit->createRow($requestedFields); 332 | } else { 333 | foreach ($hit->createRows($requestedFields, $foldedColumn) as $row) { 334 | $matches = true; 335 | if (isset($hit['highlight'])) { 336 | foreach ($this->foldedHighlights() as $column => $field) { 337 | if (isset($hit['highlight'][$field])) { 338 | $value = $row->{$foldedColumn}; 339 | if (is_string($column) && is_object($value)) { 340 | $value = $value->{$column}; 341 | } 342 | 343 | if (! in_array($value, $hit['highlight'][$field], true)) { 344 | $matches = false; 345 | break; 346 | } 347 | } 348 | } 349 | } 350 | 351 | if ($matches) { 352 | $count += 1; 353 | if ($offset === 0 || $offset < $count) { 354 | $result[] = $row; 355 | } 356 | 357 | if ($limit > 0 && $limit === count($result)) { 358 | return $result; 359 | } 360 | } 361 | } 362 | } 363 | } 364 | 365 | return $result; 366 | } 367 | 368 | /** 369 | * Return the highlight columns of an unfolded row 370 | * 371 | * @return array 372 | */ 373 | protected function foldedHighlights() 374 | { 375 | $requestedFields = $this->getColumns(); 376 | $foldedColumn = $this->getUnfoldAttribute(); 377 | if (isset($requestedFields[$foldedColumn])) { 378 | $highlightFields = array($requestedFields[$foldedColumn]); 379 | } elseif (in_array($foldedColumn, $requestedFields, true)) { 380 | $highlightFields = array($foldedColumn); 381 | } else { 382 | $highlightFields = array(); 383 | foreach ($requestedFields as $alias => $field) { 384 | if (is_string($alias) && strpos($alias, '.') !== false) { 385 | list($parent, $child) = explode('.', $alias, 2); 386 | } elseif (strpos($field, '.') !== false) { 387 | list($parent, $child) = explode('.', $field, 2); 388 | } else { 389 | continue; 390 | } 391 | 392 | if ($parent === $foldedColumn) { 393 | $highlightFields[$child] = $field; 394 | } 395 | } 396 | } 397 | 398 | return $highlightFields; 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/RestApiRequest.php: -------------------------------------------------------------------------------- 1 | headers = $headers; 64 | return $this; 65 | } 66 | 67 | /** 68 | * Return this request's headers 69 | * 70 | * @return array 71 | */ 72 | public function getHeaders() 73 | { 74 | if ($this->headers === null) { 75 | $this->headers = array(); 76 | } 77 | 78 | if (($payload = $this->getPayload())) { 79 | $this->headers[] = sprintf('Content-Length: %u', strlen($payload)); 80 | if ($this->contentType) { 81 | $this->headers[] = sprintf('Content-Type: ' . $this->contentType); 82 | } 83 | } 84 | 85 | return $this->headers; 86 | } 87 | 88 | /** 89 | * Set this request's method 90 | * 91 | * @param string $method 92 | * 93 | * @return $this 94 | */ 95 | public function setMethod($method) 96 | { 97 | $this->method = $method; 98 | return $this; 99 | } 100 | 101 | /** 102 | * Return this request's method 103 | * 104 | * @return string 105 | */ 106 | public function getMethod() 107 | { 108 | if ($this->method === null) { 109 | throw new LogicException('It is required to explicitly set a method'); 110 | } 111 | 112 | return $this->method; 113 | } 114 | 115 | /** 116 | * Set this request's path 117 | * 118 | * @param string $path 119 | * 120 | * @return $this 121 | */ 122 | public function setPath($path) 123 | { 124 | $this->path = $path; 125 | return $this; 126 | } 127 | 128 | /** 129 | * Return this request's path 130 | * 131 | * @return string 132 | */ 133 | public function getPath() 134 | { 135 | if ($this->path === null) { 136 | $this->path = ''; 137 | } 138 | 139 | return $this->path; 140 | } 141 | 142 | /** 143 | * Set this requests's parameters 144 | * 145 | * @param UrlParams $params 146 | * 147 | * @return $this 148 | */ 149 | public function setParams(UrlParams $params) 150 | { 151 | $this->params = $params; 152 | return $this; 153 | } 154 | 155 | /** 156 | * Return this request's parameters 157 | * 158 | * @return UrlParams 159 | */ 160 | public function getParams() 161 | { 162 | if ($this->params === null) { 163 | $this->params = new UrlParams(); 164 | } 165 | 166 | return $this->params; 167 | } 168 | 169 | /** 170 | * Set this request's payload 171 | * 172 | * @param string $data 173 | * @param string $contentType 174 | * 175 | * @return $this 176 | */ 177 | public function setPayload($data, $contentType = null) 178 | { 179 | $this->payload = $data; 180 | $this->contentType = $contentType; 181 | return $this; 182 | } 183 | 184 | /** 185 | * Return this request's payload 186 | * 187 | * @return string 188 | */ 189 | public function getPayload() 190 | { 191 | return $this->payload; 192 | } 193 | 194 | /** 195 | * Return the given data encoded as JSON 196 | * 197 | * @param mixed $data 198 | * 199 | * @return string 200 | * 201 | * @throws IcingaException In case the encoding has failed 202 | */ 203 | protected function jsonEncode($data) 204 | { 205 | $data = json_encode($data); 206 | if ($data !== false) { 207 | return $data; 208 | } 209 | 210 | $errorNo = json_last_error(); 211 | if ($errorNo === JSON_ERROR_CTRL_CHAR) { 212 | throw new IcingaException('Failed to encode JSON. Control character found.'); 213 | } elseif ($errorNo === JSON_ERROR_UTF8) { 214 | throw new IcingaException('Failed to encode JSON. Input is not encoded with UTF-8.'); 215 | } else { 216 | throw new IcingaException('Failed to encode JSON. Unknown error %u occurred.', $errorNo); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/RestApiResponse.php: -------------------------------------------------------------------------------- 1 | setStatusCode($statusCode); 39 | } 40 | 41 | /** 42 | * Set the status code of this response 43 | * 44 | * @param int $statusCode 45 | * 46 | * @return $this 47 | */ 48 | public function setStatusCode($statusCode) 49 | { 50 | $this->statusCode = $statusCode; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Return the status code of this response 56 | * 57 | * @return int 58 | */ 59 | public function getStatusCode() 60 | { 61 | return $this->statusCode; 62 | } 63 | 64 | /** 65 | * Set the response payload 66 | * 67 | * @param string $data 68 | * 69 | * @return $this 70 | */ 71 | public function setPayload($data) 72 | { 73 | $this->payload = $data; 74 | return $this; 75 | } 76 | 77 | /** 78 | * Return the response payload 79 | * 80 | * @return string 81 | */ 82 | public function getPayload() 83 | { 84 | return $this->payload; 85 | } 86 | 87 | /** 88 | * Set the content-type of the response payload 89 | * 90 | * @param string $contentType 91 | * 92 | * @return $this 93 | */ 94 | public function setContentType($contentType) 95 | { 96 | $this->contentType = str_replace(' ', '', $contentType); 97 | return $this; 98 | } 99 | 100 | /** 101 | * Return the content-type of the response payload 102 | * 103 | * @return string 104 | */ 105 | public function getContentType() 106 | { 107 | return $this->contentType; 108 | } 109 | 110 | /** 111 | * Return whether this is the response of a successful request 112 | * 113 | * @return bool 114 | */ 115 | public function isSuccess() 116 | { 117 | $statusCode = $this->getStatusCode(); 118 | return ($statusCode >= 200) && ($statusCode < 300); 119 | } 120 | 121 | /** 122 | * Parse the response payload as JSON and return the result 123 | * 124 | * @return mixed 125 | * 126 | * @throws IcingaException In case of an error 127 | */ 128 | public function json() 129 | { 130 | $json = json_decode($this->getPayload(), true); 131 | if ($json !== null) { 132 | return $json; 133 | } 134 | 135 | if ($this->contentType && substr($this->contentType, 0, 16) !== 'application/json') { 136 | throw new IcingaException('Cannot parse content of type "%s" as JSON', $this->contentType); 137 | } 138 | 139 | throw new IcingaException( 140 | json_last_error() === JSON_ERROR_DEPTH ? 'Too deeply nested JSON' : 'Invalid JSON' 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/SearchApiRequest.php: -------------------------------------------------------------------------------- 1 | types = $types; 42 | $this->indices = $indices; 43 | $this->setPayload($data, $data !== null ? 'application/json' : null); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getPath() 50 | { 51 | if ($this->path === null) { 52 | if (empty($this->indices)) { 53 | if (empty($this->types)) { 54 | $this->path = static::ENDPOINT; 55 | } else { 56 | $this->path = sprintf('*/%s/%s', join(',', $this->types), static::ENDPOINT); 57 | } 58 | } elseif (empty($this->types)) { 59 | $this->path = sprintf('%s/%s', join(',', $this->indices), static::ENDPOINT); 60 | } else { 61 | $this->path = sprintf( 62 | '%s/%s/%s', 63 | join(',', $this->indices), 64 | join(',', $this->types), 65 | static::ENDPOINT 66 | ); 67 | } 68 | } 69 | 70 | return $this->path; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function getPayload() 77 | { 78 | $payload = parent::getPayload(); 79 | if ($payload !== null) { 80 | $payload = $this->jsonEncode($payload); 81 | } 82 | 83 | return $payload; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/SearchHit.php: -------------------------------------------------------------------------------- 1 | data = $data; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function offsetExists($offset) 34 | { 35 | return isset($this->data[$offset]); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function offsetGet($offset) 42 | { 43 | return $this->data[$offset]; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function offsetSet($offset, $value) 50 | { 51 | $this->data[$offset] = $value; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function offsetUnset($offset) 58 | { 59 | unset($this->data[$offset]); 60 | } 61 | 62 | /** 63 | * Create and return a row for this hit 64 | * 65 | * @param array $columns 66 | * 67 | * @return object 68 | */ 69 | public function createRow(array $columns) 70 | { 71 | if (empty($columns)) { 72 | return (object) $this->data; 73 | } 74 | 75 | $row = array(); 76 | foreach ($this->treeify($columns) as $alias => $field) { 77 | if (isset($field['object_path'])) { 78 | $row[$alias] = $this->extractObject( 79 | $field['object_path'], 80 | $field['fields'], 81 | $this->data 82 | ); 83 | } else { 84 | $row[$alias] = $this->extractScalar( 85 | $field, 86 | $this->data 87 | ); 88 | } 89 | } 90 | 91 | return (object) $row; 92 | } 93 | 94 | /** 95 | * Create a row for this hit and unfold it to multiple rows using the given field 96 | * 97 | * @param array $columns 98 | * @param string $foldedColumn 99 | * 100 | * @return array 101 | */ 102 | public function createRows(array $columns, $foldedColumn) 103 | { 104 | if (strpos($foldedColumn, '.') !== false) { 105 | throw new NotImplementedError('Nested columns cannot be unfolded'); 106 | } 107 | 108 | $row = $this->createRow($columns); 109 | if (empty($row->{$foldedColumn})) { 110 | return array(); 111 | } elseif (! is_array($row->{$foldedColumn})) { 112 | return array($row); 113 | } 114 | 115 | $rows = array(); 116 | foreach ($row->{$foldedColumn} as $value) { 117 | $newRow = clone $row; 118 | $newRow->{$foldedColumn} = $value; 119 | $rows[] = $newRow; 120 | } 121 | 122 | return $rows; 123 | } 124 | 125 | /** 126 | * Extract and return one or more scalar values from the given data 127 | * 128 | * @param array $fieldPath 129 | * @param array $data 130 | * 131 | * @return mixed|array 132 | */ 133 | protected function extractScalar(array $fieldPath, array $data) 134 | { 135 | if (! $this->isAssociative($data)) { 136 | $values = array(); 137 | foreach ($data as $value) { 138 | if (is_array($value)) { 139 | $fieldValue = $this->extractScalar($fieldPath, $value); 140 | if (is_array($fieldValue)) { 141 | $values = array_merge($values, $fieldValue); 142 | } elseif ($fieldValue !== null) { 143 | $values[] = $fieldValue; 144 | } 145 | } else { 146 | Logger::debug('Expected non-scalar value but got "%s" instead', $value); 147 | } 148 | } 149 | 150 | return $values; 151 | } 152 | 153 | $field = array_shift($fieldPath); 154 | if (isset($data[$field])) { 155 | if (empty($fieldPath)) { 156 | return $data[$field]; 157 | } elseif (! is_array($data[$field])) { 158 | Logger::debug('Expected non-scalar value but got "%s" instead', $data[$field]); 159 | } else { 160 | return $this->extractScalar($fieldPath, $data[$field]); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Extract and return one or more objects from the given data 167 | * 168 | * @param array $objectPath 169 | * @param array $fields 170 | * @param array $data 171 | * 172 | * @param object|array 173 | */ 174 | protected function extractObject(array $objectPath, array $fields, array $data) 175 | { 176 | if (! $this->isAssociative($data)) { 177 | $values = array(); 178 | foreach ($data as $value) { 179 | if (is_array($value)) { 180 | $objectValue = $this->extractObject($objectPath, $fields, $value); 181 | if (is_array($objectValue)) { 182 | $values = array_merge($values, $objectValue); 183 | } elseif ($objectValue !== null) { 184 | $values[] = $objectValue; 185 | } 186 | } else { 187 | Logger::debug('Expected non-scalar value but got "%s" instead', $value); 188 | } 189 | } 190 | 191 | return $values; 192 | } 193 | 194 | $object = array_shift($objectPath); 195 | if (isset($data[$object])) { 196 | if (! is_array($data[$object])) { 197 | Logger::debug('Expected non-scalar value but got "%s" instead', $data[$object]); 198 | } elseif (! empty($objectPath)) { 199 | return $this->extractObject($objectPath, $fields, $data[$object]); 200 | } elseif ($this->isAssociative($data[$object])) { 201 | $properties = array(); 202 | foreach ($fields as $alias => $field) { 203 | if (isset($field['object_path'])) { 204 | $properties[$alias] = $this->extractObject( 205 | $field['object_path'], 206 | $field['fields'], 207 | $data[$object] 208 | ); 209 | } else { 210 | $properties[$alias] = $this->extractScalar( 211 | $field, 212 | $data[$object] 213 | ); 214 | } 215 | } 216 | 217 | return (object) $properties; 218 | } else { 219 | $objects = array(); 220 | foreach ($data[$object] as $objectData) { 221 | $properties = array(); 222 | foreach ($fields as $alias => $field) { 223 | if (isset($field['object_path'])) { 224 | $properties[$alias] = $this->extractObject( 225 | $field['object_path'], 226 | $field['fields'], 227 | $objectData 228 | ); 229 | } else { 230 | $properties[$alias] = $this->extractScalar( 231 | $field, 232 | $objectData 233 | ); 234 | } 235 | } 236 | 237 | $objects[] = (object) $properties; 238 | } 239 | 240 | return $objects; 241 | } 242 | } 243 | } 244 | 245 | /** 246 | * Turn the given flat column map into a tree 247 | * 248 | * @param array $columns 249 | * @param array $knownParents 250 | * 251 | * @return array 252 | * 253 | * @todo Make alias identifiers in $knownParents unique, may lead to strange effects otherwise in some cases 254 | */ 255 | protected function treeify(array $columns, array &$knownParents = null) 256 | { 257 | $prependSource = false; 258 | if ($knownParents === null) { 259 | $knownParents = array(); 260 | $prependSource = isset($this->data['_source']); 261 | } 262 | 263 | $tree = array(); 264 | foreach ($columns as $alias => $field) { 265 | if (! is_string($alias)) { 266 | $alias = $field; 267 | } 268 | 269 | if (strpos($alias, '.') === false) { 270 | $tree[$alias] = explode('.', $field); 271 | if ($prependSource && !$this->isMetaField($field)) { 272 | array_unshift($tree[$alias], '_source'); 273 | } 274 | } elseif (strpos($field, '.') === false) { 275 | throw new LogicException('Desired nested objects must refer to nested objects in the search result'); 276 | } else { 277 | list($aliasParent, $aliasChild) = explode('.', $alias, 2); 278 | list($fieldParent, $fieldChild) = explode('.', $field, 2); 279 | if (isset($knownParents[$aliasParent])) { 280 | $knownParents[$aliasParent]['fields'] = array_merge( 281 | $knownParents[$aliasParent]['fields'], 282 | $this->treeify(array($aliasChild, $fieldChild), $knownParents) 283 | ); 284 | } else { 285 | $tree[$aliasParent] = array( 286 | 'object_path' => $prependSource && !$this->isMetaField($fieldParent) 287 | ? array('_source', $fieldParent) 288 | : array($fieldParent), 289 | 'fields' => $this->treeify( 290 | array($aliasChild => $fieldChild), 291 | $knownParents 292 | ) 293 | ); 294 | 295 | $knownParents[$aliasParent] =& $tree[$aliasParent]; 296 | } 297 | } 298 | } 299 | 300 | return $tree; 301 | } 302 | 303 | /** 304 | * Return whether the given array is an associative one 305 | * 306 | * @param array $data 307 | * 308 | * @return bool 309 | */ 310 | protected function isAssociative(array $data) 311 | { 312 | reset($data); 313 | return is_string(key($data)); 314 | } 315 | 316 | /** 317 | * Return whether the given field is a meta field 318 | * 319 | * @param string $field 320 | * 321 | * @return bool 322 | */ 323 | protected function isMetaField($field) 324 | { 325 | return substr($field, 0, 1) === '_'; 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /library/Elasticsearch/RestApi/UpdateApiRequest.php: -------------------------------------------------------------------------------- 1 | id === null) { 21 | throw new LogicException('UpdateApiRequest is missing a document id'); 22 | } 23 | 24 | return sprintf('/%s/%s/%s/_update', $this->index, $this->documentType, $this->id); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library/vendor/Psr/Http/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | getHeaders() as $name => $values) { 51 | * echo $name . ": " . implode(", ", $values); 52 | * } 53 | * 54 | * // Emit headers iteratively: 55 | * foreach ($message->getHeaders() as $name => $values) { 56 | * foreach ($values as $value) { 57 | * header(sprintf('%s: %s', $name, $value), false); 58 | * } 59 | * } 60 | * 61 | * While header names are not case-sensitive, getHeaders() will preserve the 62 | * exact case in which headers were originally specified. 63 | * 64 | * @return array Returns an associative array of the message's headers. Each 65 | * key MUST be a header name, and each value MUST be an array of strings 66 | * for that header. 67 | */ 68 | public function getHeaders(); 69 | 70 | /** 71 | * Checks if a header exists by the given case-insensitive name. 72 | * 73 | * @param string $name Case-insensitive header field name. 74 | * @return bool Returns true if any header names match the given header 75 | * name using a case-insensitive string comparison. Returns false if 76 | * no matching header name is found in the message. 77 | */ 78 | public function hasHeader($name); 79 | 80 | /** 81 | * Retrieves a message header value by the given case-insensitive name. 82 | * 83 | * This method returns an array of all the header values of the given 84 | * case-insensitive header name. 85 | * 86 | * If the header does not appear in the message, this method MUST return an 87 | * empty array. 88 | * 89 | * @param string $name Case-insensitive header field name. 90 | * @return string[] An array of string values as provided for the given 91 | * header. If the header does not appear in the message, this method MUST 92 | * return an empty array. 93 | */ 94 | public function getHeader($name); 95 | 96 | /** 97 | * Retrieves a comma-separated string of the values for a single header. 98 | * 99 | * This method returns all of the header values of the given 100 | * case-insensitive header name as a string concatenated together using 101 | * a comma. 102 | * 103 | * NOTE: Not all header values may be appropriately represented using 104 | * comma concatenation. For such headers, use getHeader() instead 105 | * and supply your own delimiter when concatenating. 106 | * 107 | * If the header does not appear in the message, this method MUST return 108 | * an empty string. 109 | * 110 | * @param string $name Case-insensitive header field name. 111 | * @return string A string of values as provided for the given header 112 | * concatenated together using a comma. If the header does not appear in 113 | * the message, this method MUST return an empty string. 114 | */ 115 | public function getHeaderLine($name); 116 | 117 | /** 118 | * Return an instance with the provided value replacing the specified header. 119 | * 120 | * While header names are case-insensitive, the casing of the header will 121 | * be preserved by this function, and returned from getHeaders(). 122 | * 123 | * This method MUST be implemented in such a way as to retain the 124 | * immutability of the message, and MUST return an instance that has the 125 | * new and/or updated header and value. 126 | * 127 | * @param string $name Case-insensitive header field name. 128 | * @param string|string[] $value Header value(s). 129 | * @return self 130 | * @throws \InvalidArgumentException for invalid header names or values. 131 | */ 132 | public function withHeader($name, $value); 133 | 134 | /** 135 | * Return an instance with the specified header appended with the given value. 136 | * 137 | * Existing values for the specified header will be maintained. The new 138 | * value(s) will be appended to the existing list. If the header did not 139 | * exist previously, it will be added. 140 | * 141 | * This method MUST be implemented in such a way as to retain the 142 | * immutability of the message, and MUST return an instance that has the 143 | * new header and/or value. 144 | * 145 | * @param string $name Case-insensitive header field name to add. 146 | * @param string|string[] $value Header value(s). 147 | * @return self 148 | * @throws \InvalidArgumentException for invalid header names or values. 149 | */ 150 | public function withAddedHeader($name, $value); 151 | 152 | /** 153 | * Return an instance without the specified header. 154 | * 155 | * Header resolution MUST be done without case-sensitivity. 156 | * 157 | * This method MUST be implemented in such a way as to retain the 158 | * immutability of the message, and MUST return an instance that removes 159 | * the named header. 160 | * 161 | * @param string $name Case-insensitive header field name to remove. 162 | * @return self 163 | */ 164 | public function withoutHeader($name); 165 | 166 | /** 167 | * Gets the body of the message. 168 | * 169 | * @return StreamInterface Returns the body as a stream. 170 | */ 171 | public function getBody(); 172 | 173 | /** 174 | * Return an instance with the specified message body. 175 | * 176 | * The body MUST be a StreamInterface object. 177 | * 178 | * This method MUST be implemented in such a way as to retain the 179 | * immutability of the message, and MUST return a new instance that has the 180 | * new body stream. 181 | * 182 | * @param StreamInterface $body Body. 183 | * @return self 184 | * @throws \InvalidArgumentException When the body is not valid. 185 | */ 186 | public function withBody(StreamInterface $body); 187 | } 188 | -------------------------------------------------------------------------------- /library/vendor/Psr/Http/Message/RequestInterface.php: -------------------------------------------------------------------------------- 1 | getQuery()` 95 | * or from the `QUERY_STRING` server param. 96 | * 97 | * @return array 98 | */ 99 | public function getQueryParams(); 100 | 101 | /** 102 | * Return an instance with the specified query string arguments. 103 | * 104 | * These values SHOULD remain immutable over the course of the incoming 105 | * request. They MAY be injected during instantiation, such as from PHP's 106 | * $_GET superglobal, or MAY be derived from some other value such as the 107 | * URI. In cases where the arguments are parsed from the URI, the data 108 | * MUST be compatible with what PHP's parse_str() would return for 109 | * purposes of how duplicate query parameters are handled, and how nested 110 | * sets are handled. 111 | * 112 | * Setting query string arguments MUST NOT change the URI stored by the 113 | * request, nor the values in the server params. 114 | * 115 | * This method MUST be implemented in such a way as to retain the 116 | * immutability of the message, and MUST return an instance that has the 117 | * updated query string arguments. 118 | * 119 | * @param array $query Array of query string arguments, typically from 120 | * $_GET. 121 | * @return self 122 | */ 123 | public function withQueryParams(array $query); 124 | 125 | /** 126 | * Retrieve normalized file upload data. 127 | * 128 | * This method returns upload metadata in a normalized tree, with each leaf 129 | * an instance of Psr\Http\Message\UploadedFileInterface. 130 | * 131 | * These values MAY be prepared from $_FILES or the message body during 132 | * instantiation, or MAY be injected via withUploadedFiles(). 133 | * 134 | * @return array An array tree of UploadedFileInterface instances; an empty 135 | * array MUST be returned if no data is present. 136 | */ 137 | public function getUploadedFiles(); 138 | 139 | /** 140 | * Create a new instance with the specified uploaded files. 141 | * 142 | * This method MUST be implemented in such a way as to retain the 143 | * immutability of the message, and MUST return an instance that has the 144 | * updated body parameters. 145 | * 146 | * @param array An array tree of UploadedFileInterface instances. 147 | * @return self 148 | * @throws \InvalidArgumentException if an invalid structure is provided. 149 | */ 150 | public function withUploadedFiles(array $uploadedFiles); 151 | 152 | /** 153 | * Retrieve any parameters provided in the request body. 154 | * 155 | * If the request Content-Type is either application/x-www-form-urlencoded 156 | * or multipart/form-data, and the request method is POST, this method MUST 157 | * return the contents of $_POST. 158 | * 159 | * Otherwise, this method may return any results of deserializing 160 | * the request body content; as parsing returns structured content, the 161 | * potential types MUST be arrays or objects only. A null value indicates 162 | * the absence of body content. 163 | * 164 | * @return null|array|object The deserialized body parameters, if any. 165 | * These will typically be an array or object. 166 | */ 167 | public function getParsedBody(); 168 | 169 | /** 170 | * Return an instance with the specified body parameters. 171 | * 172 | * These MAY be injected during instantiation. 173 | * 174 | * If the request Content-Type is either application/x-www-form-urlencoded 175 | * or multipart/form-data, and the request method is POST, use this method 176 | * ONLY to inject the contents of $_POST. 177 | * 178 | * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of 179 | * deserializing the request body content. Deserialization/parsing returns 180 | * structured data, and, as such, this method ONLY accepts arrays or objects, 181 | * or a null value if nothing was available to parse. 182 | * 183 | * As an example, if content negotiation determines that the request data 184 | * is a JSON payload, this method could be used to create a request 185 | * instance with the deserialized parameters. 186 | * 187 | * This method MUST be implemented in such a way as to retain the 188 | * immutability of the message, and MUST return an instance that has the 189 | * updated body parameters. 190 | * 191 | * @param null|array|object $data The deserialized body data. This will 192 | * typically be in an array or object. 193 | * @return self 194 | * @throws \InvalidArgumentException if an unsupported argument type is 195 | * provided. 196 | */ 197 | public function withParsedBody($data); 198 | 199 | /** 200 | * Retrieve attributes derived from the request. 201 | * 202 | * The request "attributes" may be used to allow injection of any 203 | * parameters derived from the request: e.g., the results of path 204 | * match operations; the results of decrypting cookies; the results of 205 | * deserializing non-form-encoded message bodies; etc. Attributes 206 | * will be application and request specific, and CAN be mutable. 207 | * 208 | * @return array Attributes derived from the request. 209 | */ 210 | public function getAttributes(); 211 | 212 | /** 213 | * Retrieve a single derived request attribute. 214 | * 215 | * Retrieves a single derived request attribute as described in 216 | * getAttributes(). If the attribute has not been previously set, returns 217 | * the default value as provided. 218 | * 219 | * This method obviates the need for a hasAttribute() method, as it allows 220 | * specifying a default value to return if the attribute is not found. 221 | * 222 | * @see getAttributes() 223 | * @param string $name The attribute name. 224 | * @param mixed $default Default value to return if the attribute does not exist. 225 | * @return mixed 226 | */ 227 | public function getAttribute($name, $default = null); 228 | 229 | /** 230 | * Return an instance with the specified derived request attribute. 231 | * 232 | * This method allows setting a single derived request attribute as 233 | * described in getAttributes(). 234 | * 235 | * This method MUST be implemented in such a way as to retain the 236 | * immutability of the message, and MUST return an instance that has the 237 | * updated attribute. 238 | * 239 | * @see getAttributes() 240 | * @param string $name The attribute name. 241 | * @param mixed $value The value of the attribute. 242 | * @return self 243 | */ 244 | public function withAttribute($name, $value); 245 | 246 | /** 247 | * Return an instance that removes the specified derived request attribute. 248 | * 249 | * This method allows removing a single derived request attribute as 250 | * described in getAttributes(). 251 | * 252 | * This method MUST be implemented in such a way as to retain the 253 | * immutability of the message, and MUST return an instance that removes 254 | * the attribute. 255 | * 256 | * @see getAttributes() 257 | * @param string $name The attribute name. 258 | * @return self 259 | */ 260 | public function withoutAttribute($name); 261 | } 262 | -------------------------------------------------------------------------------- /library/vendor/Psr/Http/Message/StreamInterface.php: -------------------------------------------------------------------------------- 1 | 52 | * [user-info@]host[:port] 53 | * 54 | * 55 | * If the port component is not set or is the standard port for the current 56 | * scheme, it SHOULD NOT be included. 57 | * 58 | * @see https://tools.ietf.org/html/rfc3986#section-3.2 59 | * @return string The URI authority, in "[user-info@]host[:port]" format. 60 | */ 61 | public function getAuthority(); 62 | 63 | /** 64 | * Retrieve the user information component of the URI. 65 | * 66 | * If no user information is present, this method MUST return an empty 67 | * string. 68 | * 69 | * If a user is present in the URI, this will return that value; 70 | * additionally, if the password is also present, it will be appended to the 71 | * user value, with a colon (":") separating the values. 72 | * 73 | * The trailing "@" character is not part of the user information and MUST 74 | * NOT be added. 75 | * 76 | * @return string The URI user information, in "username[:password]" format. 77 | */ 78 | public function getUserInfo(); 79 | 80 | /** 81 | * Retrieve the host component of the URI. 82 | * 83 | * If no host is present, this method MUST return an empty string. 84 | * 85 | * The value returned MUST be normalized to lowercase, per RFC 3986 86 | * Section 3.2.2. 87 | * 88 | * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 89 | * @return string The URI host. 90 | */ 91 | public function getHost(); 92 | 93 | /** 94 | * Retrieve the port component of the URI. 95 | * 96 | * If a port is present, and it is non-standard for the current scheme, 97 | * this method MUST return it as an integer. If the port is the standard port 98 | * used with the current scheme, this method SHOULD return null. 99 | * 100 | * If no port is present, and no scheme is present, this method MUST return 101 | * a null value. 102 | * 103 | * If no port is present, but a scheme is present, this method MAY return 104 | * the standard port for that scheme, but SHOULD return null. 105 | * 106 | * @return null|int The URI port. 107 | */ 108 | public function getPort(); 109 | 110 | /** 111 | * Retrieve the path component of the URI. 112 | * 113 | * The path can either be empty or absolute (starting with a slash) or 114 | * rootless (not starting with a slash). Implementations MUST support all 115 | * three syntaxes. 116 | * 117 | * Normally, the empty path "" and absolute path "/" are considered equal as 118 | * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically 119 | * do this normalization because in contexts with a trimmed base path, e.g. 120 | * the front controller, this difference becomes significant. It's the task 121 | * of the user to handle both "" and "/". 122 | * 123 | * The value returned MUST be percent-encoded, but MUST NOT double-encode 124 | * any characters. To determine what characters to encode, please refer to 125 | * RFC 3986, Sections 2 and 3.3. 126 | * 127 | * As an example, if the value should include a slash ("/") not intended as 128 | * delimiter between path segments, that value MUST be passed in encoded 129 | * form (e.g., "%2F") to the instance. 130 | * 131 | * @see https://tools.ietf.org/html/rfc3986#section-2 132 | * @see https://tools.ietf.org/html/rfc3986#section-3.3 133 | * @return string The URI path. 134 | */ 135 | public function getPath(); 136 | 137 | /** 138 | * Retrieve the query string of the URI. 139 | * 140 | * If no query string is present, this method MUST return an empty string. 141 | * 142 | * The leading "?" character is not part of the query and MUST NOT be 143 | * added. 144 | * 145 | * The value returned MUST be percent-encoded, but MUST NOT double-encode 146 | * any characters. To determine what characters to encode, please refer to 147 | * RFC 3986, Sections 2 and 3.4. 148 | * 149 | * As an example, if a value in a key/value pair of the query string should 150 | * include an ampersand ("&") not intended as a delimiter between values, 151 | * that value MUST be passed in encoded form (e.g., "%26") to the instance. 152 | * 153 | * @see https://tools.ietf.org/html/rfc3986#section-2 154 | * @see https://tools.ietf.org/html/rfc3986#section-3.4 155 | * @return string The URI query string. 156 | */ 157 | public function getQuery(); 158 | 159 | /** 160 | * Retrieve the fragment component of the URI. 161 | * 162 | * If no fragment is present, this method MUST return an empty string. 163 | * 164 | * The leading "#" character is not part of the fragment and MUST NOT be 165 | * added. 166 | * 167 | * The value returned MUST be percent-encoded, but MUST NOT double-encode 168 | * any characters. To determine what characters to encode, please refer to 169 | * RFC 3986, Sections 2 and 3.5. 170 | * 171 | * @see https://tools.ietf.org/html/rfc3986#section-2 172 | * @see https://tools.ietf.org/html/rfc3986#section-3.5 173 | * @return string The URI fragment. 174 | */ 175 | public function getFragment(); 176 | 177 | /** 178 | * Return an instance with the specified scheme. 179 | * 180 | * This method MUST retain the state of the current instance, and return 181 | * an instance that contains the specified scheme. 182 | * 183 | * Implementations MUST support the schemes "http" and "https" case 184 | * insensitively, and MAY accommodate other schemes if required. 185 | * 186 | * An empty scheme is equivalent to removing the scheme. 187 | * 188 | * @param string $scheme The scheme to use with the new instance. 189 | * @return self A new instance with the specified scheme. 190 | * @throws \InvalidArgumentException for invalid or unsupported schemes. 191 | */ 192 | public function withScheme($scheme); 193 | 194 | /** 195 | * Return an instance with the specified user information. 196 | * 197 | * This method MUST retain the state of the current instance, and return 198 | * an instance that contains the specified user information. 199 | * 200 | * Password is optional, but the user information MUST include the 201 | * user; an empty string for the user is equivalent to removing user 202 | * information. 203 | * 204 | * @param string $user The user name to use for authority. 205 | * @param null|string $password The password associated with $user. 206 | * @return self A new instance with the specified user information. 207 | */ 208 | public function withUserInfo($user, $password = null); 209 | 210 | /** 211 | * Return an instance with the specified host. 212 | * 213 | * This method MUST retain the state of the current instance, and return 214 | * an instance that contains the specified host. 215 | * 216 | * An empty host value is equivalent to removing the host. 217 | * 218 | * @param string $host The hostname to use with the new instance. 219 | * @return self A new instance with the specified host. 220 | * @throws \InvalidArgumentException for invalid hostnames. 221 | */ 222 | public function withHost($host); 223 | 224 | /** 225 | * Return an instance with the specified port. 226 | * 227 | * This method MUST retain the state of the current instance, and return 228 | * an instance that contains the specified port. 229 | * 230 | * Implementations MUST raise an exception for ports outside the 231 | * established TCP and UDP port ranges. 232 | * 233 | * A null value provided for the port is equivalent to removing the port 234 | * information. 235 | * 236 | * @param null|int $port The port to use with the new instance; a null value 237 | * removes the port information. 238 | * @return self A new instance with the specified port. 239 | * @throws \InvalidArgumentException for invalid ports. 240 | */ 241 | public function withPort($port); 242 | 243 | /** 244 | * Return an instance with the specified path. 245 | * 246 | * This method MUST retain the state of the current instance, and return 247 | * an instance that contains the specified path. 248 | * 249 | * The path can either be empty or absolute (starting with a slash) or 250 | * rootless (not starting with a slash). Implementations MUST support all 251 | * three syntaxes. 252 | * 253 | * If the path is intended to be domain-relative rather than path relative then 254 | * it must begin with a slash ("/"). Paths not starting with a slash ("/") 255 | * are assumed to be relative to some base path known to the application or 256 | * consumer. 257 | * 258 | * Users can provide both encoded and decoded path characters. 259 | * Implementations ensure the correct encoding as outlined in getPath(). 260 | * 261 | * @param string $path The path to use with the new instance. 262 | * @return self A new instance with the specified path. 263 | * @throws \InvalidArgumentException for invalid paths. 264 | */ 265 | public function withPath($path); 266 | 267 | /** 268 | * Return an instance with the specified query string. 269 | * 270 | * This method MUST retain the state of the current instance, and return 271 | * an instance that contains the specified query string. 272 | * 273 | * Users can provide both encoded and decoded query characters. 274 | * Implementations ensure the correct encoding as outlined in getQuery(). 275 | * 276 | * An empty query string value is equivalent to removing the query string. 277 | * 278 | * @param string $query The query string to use with the new instance. 279 | * @return self A new instance with the specified query string. 280 | * @throws \InvalidArgumentException for invalid query strings. 281 | */ 282 | public function withQuery($query); 283 | 284 | /** 285 | * Return an instance with the specified URI fragment. 286 | * 287 | * This method MUST retain the state of the current instance, and return 288 | * an instance that contains the specified URI fragment. 289 | * 290 | * Users can provide both encoded and decoded fragment characters. 291 | * Implementations ensure the correct encoding as outlined in getFragment(). 292 | * 293 | * An empty fragment value is equivalent to removing the fragment. 294 | * 295 | * @param string $fragment The fragment to use with the new instance. 296 | * @return self A new instance with the specified fragment. 297 | */ 298 | public function withFragment($fragment); 299 | 300 | /** 301 | * Return the string representation as a URI reference. 302 | * 303 | * Depending on which components of the URI are present, the resulting 304 | * string is either a full URI or relative reference according to RFC 3986, 305 | * Section 4.1. The method concatenates the various components of the URI, 306 | * using the appropriate delimiters: 307 | * 308 | * - If a scheme is present, it MUST be suffixed by ":". 309 | * - If an authority is present, it MUST be prefixed by "//". 310 | * - The path can be concatenated without delimiters. But there are two 311 | * cases where the path has to be adjusted to make the URI reference 312 | * valid as PHP does not allow to throw an exception in __toString(): 313 | * - If the path is rootless and an authority is present, the path MUST 314 | * be prefixed by "/". 315 | * - If the path is starting with more than one "/" and no authority is 316 | * present, the starting slashes MUST be reduced to one. 317 | * - If a query is present, it MUST be prefixed by "?". 318 | * - If a fragment is present, it MUST be prefixed by "#". 319 | * 320 | * @see http://tools.ietf.org/html/rfc3986#section-4.1 321 | * @return string 322 | */ 323 | public function __toString(); 324 | } 325 | -------------------------------------------------------------------------------- /library/vendor/Psr/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 PHP Framework Interoperability Group 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /library/vendor/Psr/Loader.php: -------------------------------------------------------------------------------- 1 | getHeaders() as $name => $values) { 63 | $headers[] = $name . ': ' . implode(', ', $values); 64 | } 65 | 66 | $curlOptions = [ 67 | CURLOPT_FOLLOWLOCATION => true, 68 | CURLOPT_RETURNTRANSFER => false, 69 | CURLOPT_FAILONERROR => true, 70 | CURLOPT_USERAGENT => $this->getAgent() 71 | ]; 72 | 73 | if (isset($options['curl'])) { 74 | $curlOptions += $options['curl']; 75 | } 76 | 77 | $curlOptions += [ 78 | CURLOPT_CUSTOMREQUEST => $request->getMethod(), 79 | CURLOPT_HTTPHEADER => $headers, 80 | CURLOPT_URL => (string) $request->getUri()->withFragment('') 81 | ]; 82 | 83 | if (! $request->hasHeader('Accept')) { 84 | $curlOptions[CURLOPT_HTTPHEADER][] = 'Accept:'; 85 | } 86 | 87 | if (! $request->hasHeader('Content-Type')) { 88 | $curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:'; 89 | } 90 | 91 | if (! $request->hasHeader('Expect')) { 92 | $curlOptions[CURLOPT_HTTPHEADER][] = 'Expect:'; 93 | } 94 | 95 | if ($request->getBody()->getSize() !== 0) { 96 | $curlOptions[CURLOPT_UPLOAD] = true; 97 | 98 | $body = $request->getBody(); 99 | if ($body->isSeekable()) { 100 | $body->seek(0); 101 | } 102 | 103 | $curlOptions[CURLOPT_READFUNCTION] = function ($ch, $infile, $length) use ($body) { 104 | return $body->read($length); 105 | }; 106 | } 107 | 108 | if ($request->getProtocolVersion()) { 109 | $protocolVersion = null; 110 | switch ($request->getProtocolVersion()) { 111 | case '2.0': 112 | if (version_compare(phpversion(), '7.0.7', '<')) { 113 | throw new RuntimeException('You need at least PHP 7.0.7 to use HTTP 2.0'); 114 | } 115 | $protocolVersion = CURL_HTTP_VERSION_2; 116 | break; 117 | case '1.1': 118 | $protocolVersion = CURL_HTTP_VERSION_1_1; 119 | break; 120 | default: 121 | $protocolVersion = CURL_HTTP_VERSION_1_0; 122 | } 123 | 124 | $curlOptions[CURLOPT_HTTP_VERSION] = $protocolVersion; 125 | } 126 | 127 | $handle = new Handle(); 128 | 129 | $curlOptions[CURLOPT_HEADERFUNCTION] = function($ch, $header) use ($handle) { 130 | $size = strlen($header); 131 | 132 | if (! trim($header) || strpos($header, 'HTTP/') === 0) { 133 | return $size; 134 | } 135 | 136 | list($key, $value) = explode(': ', $header, 2); 137 | $handle->responseHeaders[$key] = rtrim($value, "\r\n"); 138 | 139 | return $size; 140 | }; 141 | 142 | $handle->responseBody = Stream::open(); 143 | 144 | $curlOptions[CURLOPT_WRITEFUNCTION] = function ($ch, $string) use ($handle) { 145 | return $handle->responseBody->write($string); 146 | }; 147 | 148 | $ch = ! empty($this->handles) ? array_pop($this->handles) : curl_init(); 149 | 150 | curl_setopt_array($ch, $curlOptions); 151 | 152 | $handle->handle = $ch; 153 | 154 | return $handle; 155 | } 156 | 157 | /** 158 | * Execute a cURL handle and return the response 159 | * 160 | * @param Handle $handle 161 | * 162 | * @return ResponseInterface 163 | * 164 | * @throws RuntimeException 165 | */ 166 | protected function executeHandle(Handle $handle) 167 | { 168 | $ch = $handle->handle; 169 | 170 | $success = curl_exec($ch); 171 | 172 | if ($success === false) { 173 | throw new RuntimeException(curl_error($ch)); 174 | } 175 | 176 | $response = new Response( 177 | curl_getinfo($ch, CURLINFO_HTTP_CODE), $handle->responseHeaders, $handle->responseBody 178 | ); 179 | 180 | if (count($this->handles) >= self::MAX_HANDLES) { 181 | curl_close($ch); 182 | } else { 183 | curl_reset($ch); 184 | 185 | $this->handles[] = $ch; 186 | } 187 | 188 | return $response; 189 | } 190 | 191 | public function send(RequestInterface $request, array $options = []) 192 | { 193 | $handle = $this->createHandle($request, $options); 194 | 195 | $response = $this->executeHandle($handle); 196 | 197 | return $response; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /library/vendor/iplx/Http/ClientInterface.php: -------------------------------------------------------------------------------- 1 | protocolVersion; 40 | } 41 | 42 | public function withProtocolVersion($version) 43 | { 44 | $message = clone $this; 45 | $message->protocolVersion = $version; 46 | 47 | return $message; 48 | } 49 | 50 | public function getHeaders() 51 | { 52 | return array_combine($this->headerNames, $this->headerValues); 53 | } 54 | 55 | public function hasHeader($header) 56 | { 57 | return isset($this->headerValues[strtolower($header)]); 58 | } 59 | 60 | public function getHeader($header) 61 | { 62 | $header = strtolower($header); 63 | 64 | if (! isset($this->headerValues[$header])) { 65 | return []; 66 | } 67 | 68 | return $this->headerValues[$header]; 69 | } 70 | 71 | public function getHeaderLine($name) 72 | { 73 | $name = strtolower($name); 74 | 75 | if (! isset($this->headerValues[$name])) { 76 | return ''; 77 | } 78 | 79 | return implode(', ', $this->headerValues[$name]); 80 | } 81 | 82 | public function withHeader($name, $value) 83 | { 84 | $name = rtrim($name); 85 | 86 | $value = $this->normalizeHeaderValues($value); 87 | 88 | $normalized = strtolower($name); 89 | 90 | $message = clone $this; 91 | $message->headerNames[$normalized] = $name; 92 | $message->headerValues[$normalized] = $value; 93 | 94 | return $message; 95 | } 96 | 97 | public function withAddedHeader($name, $value) 98 | { 99 | $name = rtrim($name); 100 | 101 | $value = $this->normalizeHeaderValues($value); 102 | 103 | $normalized = strtolower($name); 104 | 105 | $message = clone $this; 106 | if (isset($message->headerNames[$normalized])) { 107 | $message->headerValues[$normalized] = array_merge($message->headerValues[$normalized], $value); 108 | } else { 109 | $message->headerNames[$normalized] = $name; 110 | $message->headerValues[$normalized] = $value; 111 | } 112 | 113 | return $message; 114 | } 115 | 116 | public function withoutHeader($name) 117 | { 118 | $normalized = strtolower(rtrim($name)); 119 | 120 | $message = clone $this; 121 | unset($message->headerNames[$normalized]); 122 | unset($message->headerValues[$normalized]); 123 | 124 | return $message; 125 | } 126 | 127 | public function getBody() 128 | { 129 | return $this->body; 130 | } 131 | 132 | public function withBody(StreamInterface $body) 133 | { 134 | $message = clone $this; 135 | $message->body = $body; 136 | 137 | return $message; 138 | } 139 | 140 | protected function setHeaders(array $headers) 141 | { 142 | // Prepare header field names and header field values according to 143 | // https://tools.ietf.org/html/rfc7230#section-3.2.4 144 | $names = array_map('rtrim', array_keys($headers)); 145 | $values = $this->normalizeHeaderValues($headers); 146 | 147 | $normalized = array_map('strtolower', $names); 148 | 149 | $this->headerNames = array_combine( 150 | $normalized, 151 | $names 152 | ); 153 | 154 | $this->headerValues = array_combine( 155 | $normalized, 156 | $values 157 | ); 158 | } 159 | 160 | protected function normalizeHeaderValues(array $values) 161 | { 162 | // Prepare header field names and header field values according to 163 | // https://tools.ietf.org/html/rfc7230#section-3.2.4 164 | return array_map(function ($value) { 165 | if (! is_array($value)) { 166 | $value = [$value]; 167 | } 168 | 169 | return array_map(function ($value) { 170 | return trim($value, " \t"); 171 | }, $value); 172 | }, $values); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /library/vendor/iplx/Http/Request.php: -------------------------------------------------------------------------------- 1 | method = $method; 48 | $this->uri = new Uri($uri); 49 | $this->setHeaders($headers); 50 | $this->body = Stream::create($body); 51 | $this->protocolVersion = $protocolVersion; 52 | 53 | $this->provideHostHeader(); 54 | } 55 | 56 | public function getRequestTarget() 57 | { 58 | if ($this->requestTarget !== null) { 59 | return $this->requestTarget; 60 | } 61 | 62 | $requestTarget = $this->uri->getPath(); 63 | 64 | // Weak type checks to also check null 65 | 66 | if ($requestTarget == '') { 67 | $requestTarget = '/'; 68 | } 69 | 70 | if ($this->uri->getQuery() != '') { 71 | $requestTarget .= '?' . $this->uri->getQuery(); 72 | } 73 | 74 | return $requestTarget; 75 | } 76 | 77 | public function withRequestTarget($requestTarget) 78 | { 79 | $request = clone $this; 80 | $request->requestTarget = $requestTarget; 81 | 82 | return $request; 83 | } 84 | 85 | public function getMethod() 86 | { 87 | return $this->method; 88 | } 89 | 90 | public function withMethod($method) 91 | { 92 | $request = clone $this; 93 | $request->method = $method; 94 | 95 | return $this; 96 | } 97 | 98 | public function getUri() 99 | { 100 | return $this->uri; 101 | } 102 | 103 | public function withUri(UriInterface $uri, $preserveHost = false) 104 | { 105 | $request = clone $this; 106 | $request->uri = $uri; 107 | 108 | if (! $preserveHost) { 109 | $this->provideHostHeader(true); 110 | } 111 | 112 | return $this; 113 | } 114 | 115 | protected function provideHostHeader($force = false) 116 | { 117 | if ($this->hasHeader('host')) { 118 | if (! $force) { 119 | return; 120 | } 121 | 122 | $header = $this->headerNames['host']; 123 | } else { 124 | $header = 'Host'; 125 | } 126 | 127 | $host = $this->uri->getHost(); 128 | 129 | // Weak type check to also check null 130 | if ($host == '') { 131 | $host = ''; 132 | } else { 133 | $port = $this->uri->getPort(); 134 | 135 | if ($port !== null) { 136 | $host .= ":$port"; 137 | } 138 | } 139 | 140 | $this->headerNames['host'] = $header; 141 | $this->headerValues['host'] = [$host]; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /library/vendor/iplx/Http/Response.php: -------------------------------------------------------------------------------- 1 | statusCode = $statusCode; 40 | $this->setHeaders($headers); 41 | $this->body = Stream::create($body); 42 | $this->protocolVersion = $protocolVersion; 43 | $this->reasonPhrase = $reasonPhrase; 44 | } 45 | 46 | public function getStatusCode() 47 | { 48 | return $this->statusCode; 49 | } 50 | 51 | public function withStatus($code, $reasonPhrase = '') 52 | { 53 | $response = clone $this; 54 | $response->statusCode = $code; 55 | $response->reasonPhrase = $reasonPhrase; 56 | 57 | return $response; 58 | } 59 | 60 | public function getReasonPhrase() 61 | { 62 | return $this->reasonPhrase; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /library/vendor/iplx/Http/Stream.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 29 | 30 | $meta = stream_get_meta_data($this->stream); 31 | $this->seekable = $meta['seekable']; 32 | $this->readable = preg_match('/[r+]/', $meta['mode']) === 1; 33 | $this->writable = preg_match('/[waxc+]/', $meta['mode']) === 1; 34 | } 35 | 36 | public function __destruct() 37 | { 38 | $this->close(); 39 | } 40 | 41 | public function __toString() 42 | { 43 | try { 44 | $this->seek(0); 45 | $contents = stream_get_contents($this->stream); 46 | } catch (Exception $e) { 47 | $contents = ''; 48 | } 49 | 50 | return $contents; 51 | } 52 | 53 | public function close() 54 | { 55 | if (isset($this->stream)) { 56 | if (is_resource($this->stream)) { 57 | fclose($this->stream); 58 | } 59 | $this->detach(); 60 | } 61 | } 62 | 63 | public function detach() 64 | { 65 | if (! isset($this->stream)) { 66 | return null; 67 | } 68 | 69 | $stream = $this->stream; 70 | 71 | $this->stream = null; 72 | $this->size = null; 73 | $this->seekable = false; 74 | $this->readable = false; 75 | $this->writable = false; 76 | 77 | return $stream; 78 | } 79 | 80 | public function getSize() 81 | { 82 | if ($this->size !== null) { 83 | return $this->size; 84 | } 85 | 86 | if (! isset($this->stream)) { 87 | return null; 88 | } 89 | 90 | $stats = fstat($this->stream); 91 | $this->size = $stats['size']; 92 | 93 | return $this->size; 94 | } 95 | 96 | public function tell() 97 | { 98 | $this->assertAttached(); 99 | 100 | $position = ftell($this->stream); 101 | 102 | if ($position === false) { 103 | throw new RuntimeException('Unable to determine stream position'); 104 | } 105 | 106 | return $position; 107 | } 108 | 109 | public function eof() 110 | { 111 | $this->assertAttached(); 112 | 113 | return feof($this->stream); 114 | } 115 | 116 | public function isSeekable() 117 | { 118 | return $this->seekable; 119 | } 120 | 121 | public function seek($offset, $whence = SEEK_SET) 122 | { 123 | $this->assertSeekable(); 124 | 125 | if (fseek($this->stream, $offset, $whence) === -1) { 126 | throw new RuntimeException('Unable to seek to stream position'); 127 | } 128 | } 129 | 130 | public function rewind() 131 | { 132 | $this->seek(0); 133 | } 134 | 135 | public function isWritable() 136 | { 137 | return $this->writable; 138 | } 139 | 140 | public function write($string) 141 | { 142 | $this->assertWritable(); 143 | 144 | $written = fwrite($this->stream, $string); 145 | 146 | if ($written === false) { 147 | throw new RuntimeException('Unable to write to stream'); 148 | } 149 | 150 | return $written; 151 | } 152 | 153 | public function isReadable() 154 | { 155 | return $this->readable; 156 | } 157 | 158 | public function read($length) 159 | { 160 | $this->assertReadable(); 161 | 162 | $data = fread($this->stream, $length); 163 | 164 | if ($data === false) { 165 | throw new RuntimeException('Unable to read from stream'); 166 | } 167 | 168 | return $data; 169 | } 170 | 171 | public function getContents() 172 | { 173 | $this->assertReadable(); 174 | 175 | $contents = stream_get_contents($this->stream); 176 | 177 | if ($contents === false) { 178 | throw new RuntimeException('Unable to read stream contents'); 179 | } 180 | 181 | return $contents; 182 | } 183 | 184 | public function getMetadata($key = null) 185 | { 186 | if (! isset($this->stream)) { 187 | return $key === null ? [] : null; 188 | } 189 | 190 | $meta = stream_get_meta_data($this->stream); 191 | 192 | if ($key === null) { 193 | return $meta; 194 | } 195 | 196 | if (isset($meta[$key])) { 197 | return $meta[$key]; 198 | } 199 | 200 | return null; 201 | } 202 | 203 | public function assertAttached() 204 | { 205 | if (! isset($this->stream)) { 206 | throw new RuntimeException('Stream is detached'); 207 | } 208 | } 209 | 210 | public function assertSeekable() 211 | { 212 | $this->assertAttached(); 213 | 214 | if (! $this->isSeekable()) { 215 | throw new RuntimeException('Stream is not seekable'); 216 | } 217 | } 218 | 219 | public function assertReadable() 220 | { 221 | $this->assertAttached(); 222 | 223 | if (! $this->isReadable()) { 224 | throw new RuntimeException('Stream is not readable'); 225 | } 226 | } 227 | 228 | public function assertWritable() 229 | { 230 | $this->assertAttached(); 231 | 232 | if (! $this->isWritable()) { 233 | throw new RuntimeException('Stream is not writable'); 234 | } 235 | } 236 | 237 | /** 238 | * Open a stream 239 | * 240 | * @param string $filename 241 | * @param string $mode 242 | * 243 | * @return static 244 | */ 245 | public static function open($filename = 'php://temp', $mode = 'r+') 246 | { 247 | $stream = fopen($filename, $mode); 248 | 249 | return new static($stream); 250 | } 251 | 252 | /** 253 | * Create a stream 254 | * 255 | * @param StreamInterface|string|resource $resource 256 | * 257 | * @return StreamInterface 258 | */ 259 | public static function create($resource) 260 | { 261 | if ($resource instanceof StreamInterface) { 262 | return $resource; 263 | } 264 | 265 | if (is_scalar($resource)) { 266 | $stream = fopen('php://temp', 'r+'); 267 | 268 | if ($resource !== '') { 269 | fwrite($stream, $resource); 270 | fseek($stream, 0); 271 | } 272 | 273 | return new static($stream); 274 | } 275 | 276 | if (is_resource($resource)) { 277 | return new static($resource); 278 | } 279 | 280 | return static::open(); 281 | } 282 | 283 | } 284 | -------------------------------------------------------------------------------- /library/vendor/iplx/Http/Uri.php: -------------------------------------------------------------------------------- 1 | $value) { 35 | $this->$component = $value; 36 | } 37 | } 38 | 39 | public function getScheme() 40 | { 41 | return $this->scheme; 42 | } 43 | 44 | public function getAuthority() 45 | { 46 | // Weak type check to also check null 47 | if ($this->host == '') { 48 | return ''; 49 | } 50 | 51 | $authority = $this->host; 52 | 53 | $userInfo = $this->getUserInfo(); 54 | $port = $this->getPort(); 55 | 56 | if ($userInfo) { 57 | $authority = "$userInfo@$authority"; 58 | } 59 | 60 | if ($port !== null) { 61 | $authority .= ":$port"; 62 | } 63 | 64 | return $authority; 65 | } 66 | 67 | public function getUserInfo() 68 | { 69 | $userInfo = $this->user; 70 | 71 | if ($this->pass !== null) { 72 | $userInfo .= ":{$this->pass}"; 73 | } 74 | 75 | return $userInfo; 76 | } 77 | 78 | public function getHost() 79 | { 80 | return $this->host; 81 | } 82 | 83 | public function getPort() 84 | { 85 | return $this->port; 86 | } 87 | 88 | public function getPath() 89 | { 90 | return $this->path; 91 | } 92 | 93 | public function getQuery() 94 | { 95 | return $this->query; 96 | } 97 | 98 | public function getFragment() 99 | { 100 | return $this->fragment; 101 | } 102 | 103 | public function withScheme($scheme) 104 | { 105 | $uri = clone $this; 106 | $uri->scheme = $scheme; 107 | 108 | return $uri; 109 | } 110 | 111 | public function withUserInfo($user, $password = null) 112 | { 113 | $uri = clone $this; 114 | $uri->user = $user; 115 | $uri->pass = $password; 116 | 117 | return $uri; 118 | } 119 | 120 | public function withHost($host) 121 | { 122 | $uri = clone $this; 123 | $uri->host = $host; 124 | 125 | return $uri; 126 | } 127 | 128 | public function withPort($port) 129 | { 130 | $uri = clone $this; 131 | $uri->port = $port; 132 | 133 | return $uri; 134 | } 135 | 136 | public function withPath($path) 137 | { 138 | $uri = clone $this; 139 | $uri->path = $path; 140 | 141 | return $uri; 142 | } 143 | 144 | public function withQuery($query) 145 | { 146 | $uri = clone $this; 147 | $uri->query = $query; 148 | 149 | return $uri; 150 | } 151 | 152 | public function withFragment($fragment) 153 | { 154 | $uri = clone $this; 155 | $uri->fragment = $fragment; 156 | 157 | return $uri; 158 | } 159 | 160 | public function __toString() 161 | { 162 | $scheme = $this->getScheme(); 163 | $authority = $this->getAuthority(); 164 | $path = $this->getPath(); 165 | $query = $this->getQuery(); 166 | $fragment = $this->getFragment(); 167 | 168 | $uri = ''; 169 | 170 | // Weak type checks to also check null 171 | 172 | if ($scheme != '') { 173 | $uri = "$scheme:"; 174 | } 175 | 176 | if ($authority != '') { 177 | $uri .= "//$authority"; 178 | } 179 | 180 | if ($path != '') { 181 | if ($path[0] === '/') { 182 | if ($authority == '') { 183 | $path = ltrim($path, '/'); 184 | } 185 | } else { 186 | $path = "/$path"; 187 | } 188 | 189 | $uri .= $path; 190 | } 191 | 192 | if ($query != '') { 193 | $uri .= "?$query"; 194 | } 195 | 196 | if ($fragment != '') { 197 | $uri .= "#$fragment"; 198 | } 199 | 200 | return $uri; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /library/vendor/iplx/Loader.php: -------------------------------------------------------------------------------- 1 | .control-group { 9 | padding: 0; 10 | 11 | > .control-label-group { 12 | text-align: left; 13 | padding: 0; 14 | width: 1.2em; 15 | } 16 | 17 | > select { 18 | width: 4.5em; 19 | } 20 | 21 | > i { 22 | .sr-only(); 23 | } 24 | } 25 | } 26 | 27 | .actions { 28 | margin-bottom: 1em; 29 | } 30 | 31 | .grid { 32 | align-items: stretch; 33 | display: flex; 34 | flex-wrap: wrap; 35 | } 36 | 37 | .grid-item { 38 | background-clip: padding-box; 39 | border-right: 1em solid transparent; 40 | border-bottom: 1em solid transparent; 41 | justify-content: stretch; 42 | width: 20em; 43 | 44 | > .button-link { 45 | display: block; 46 | margin-bottom: 0.5em; 47 | } 48 | } 49 | 50 | .grid-item-header { 51 | background: #ef4f98; 52 | color: white; 53 | display: flex; 54 | flex-direction: column; 55 | height: 3em; 56 | justify-content: center; 57 | margin: 0; 58 | text-align: center; 59 | text-transform: uppercase; 60 | vertical-align: middle; 61 | } 62 | 63 | .missing-configuration-error { 64 | background-color: @color-unknown; 65 | color: #fff; 66 | display: inline-block; 67 | padding: 1em; 68 | } 69 | 70 | .eventtype-control { 71 | margin-bottom: 1em; 72 | 73 | > .control-group { 74 | padding: 0; 75 | 76 | > .control-label-group { 77 | text-align: left; 78 | width: auto; 79 | } 80 | 81 | > i { 82 | .sr-only(); 83 | } 84 | } 85 | } 86 | 87 | .events-table { 88 | width: 100%; 89 | 90 | > thead > tr > th { 91 | text-align: left; 92 | } 93 | 94 | > tbody tr:nth-child(odd) { 95 | background-color: @gray-lightest; 96 | } 97 | 98 | > tbody { 99 | td { 100 | padding: 0.5em; 101 | white-space: nowrap; 102 | } 103 | 104 | tr { 105 | border-left: 5px solid transparent; 106 | } 107 | 108 | tr[href].active { 109 | background-color: @tr-active-color; 110 | border-left-color: @icinga-blue; 111 | } 112 | 113 | tr[href]:hover { 114 | background-color: @tr-hover-color; 115 | cursor: pointer; 116 | } 117 | } 118 | } 119 | 120 | .document { 121 | th { 122 | text-align: left; 123 | } 124 | 125 | td, th { 126 | padding: 0.5em 0 0 0; 127 | vertical-align: top; 128 | } 129 | 130 | tr:first-child { 131 | > td, th { 132 | padding-top: 0; 133 | } 134 | } 135 | 136 | th, .value { 137 | padding-left: 1em; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /run.php: -------------------------------------------------------------------------------- 1 | getLibDir() . '/vendor/Psr/Loader.php'; 7 | require_once $this->getLibDir() . '/vendor/iplx/Loader.php'; 8 | 9 | $this->provideHook('monitoring/HostActions'); 10 | --------------------------------------------------------------------------------