summaryrefslogtreecommitdiffstats
path: root/library/Icingadb/Common/ObjectInspectionDetail.php
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--library/Icingadb/Common/ObjectInspectionDetail.php348
1 files changed, 348 insertions, 0 deletions
diff --git a/library/Icingadb/Common/ObjectInspectionDetail.php b/library/Icingadb/Common/ObjectInspectionDetail.php
new file mode 100644
index 0000000..b30797b
--- /dev/null
+++ b/library/Icingadb/Common/ObjectInspectionDetail.php
@@ -0,0 +1,348 @@
+<?php
+
+/* Icinga DB Web | (c) 2021 Icinga GmbH | GPLv2 */
+
+namespace Icinga\Module\Icingadb\Common;
+
+use DateTime;
+use DateTimeZone;
+use Exception;
+use Icinga\Exception\IcingaException;
+use Icinga\Exception\Json\JsonDecodeException;
+use Icinga\Module\Icingadb\Model\Host;
+use Icinga\Module\Icingadb\Model\Service;
+use Icinga\Module\Icingadb\Widget\Detail\CustomVarTable;
+use Icinga\Util\Format;
+use Icinga\Util\Json;
+use InvalidArgumentException;
+use ipl\Html\BaseHtmlElement;
+use ipl\Html\FormattedString;
+use ipl\Html\HtmlElement;
+use ipl\Html\Table;
+use ipl\Html\Text;
+use ipl\Orm\Model;
+use ipl\Web\Widget\CopyToClipboard;
+use ipl\Web\Widget\EmptyState;
+
+abstract class ObjectInspectionDetail extends BaseHtmlElement
+{
+ protected $tag = 'div';
+
+ protected $defaultAttributes = ['class' => ['object-detail', 'inspection-detail']];
+
+ /** @var Model */
+ protected $object;
+
+ /** @var array */
+ protected $attrs;
+
+ /** @var array */
+ protected $joins;
+
+ public function __construct(Model $object, array $apiResult)
+ {
+ $this->object = $object;
+ $this->attrs = $apiResult['attrs'];
+ $this->joins = $apiResult['joins'];
+ }
+
+ /**
+ * Render the object source location
+ *
+ * @return ?array
+ */
+ protected function createSourceLocation()
+ {
+ if (! isset($this->attrs['source_location'])) {
+ return;
+ }
+
+ return [
+ new HtmlElement('h2', null, Text::create(t('Source Location'))),
+ FormattedString::create(
+ t('You can find this object in %s on line %s.'),
+ new HtmlElement('strong', null, Text::create($this->attrs['source_location']['path'])),
+ new HtmlElement('strong', null, Text::create($this->attrs['source_location']['first_line']))
+ )
+ ];
+ }
+
+ /**
+ * Render object's last check result
+ *
+ * @return ?array
+ */
+ protected function createLastCheckResult()
+ {
+ if (! isset($this->attrs['last_check_result'])) {
+ return;
+ }
+
+ $command = $this->attrs['last_check_result']['command'];
+ if (is_array($command)) {
+ $command = join(' ', array_map('escapeshellarg', $command));
+ }
+
+ $denylist = [
+ 'command',
+ 'output',
+ 'type',
+ 'active'
+ ];
+
+ if ($command) {
+ $execCommand = new HtmlElement('pre', null, Text::create($command));
+ CopyToClipboard::attachTo($execCommand);
+ } else {
+ $execCommand = new EmptyState(t('n. a.'));
+ }
+
+ return [
+ new HtmlElement('h2', null, Text::create(t('Executed Command'))),
+ $execCommand,
+ new HtmlElement('h2', null, Text::create(t('Execution Details'))),
+ $this->createNameValueTable(
+ array_diff_key($this->attrs['last_check_result'], array_flip($denylist)),
+ [
+ 'execution_end' => [$this, 'formatTimestamp'],
+ 'execution_start' => [$this, 'formatTimestamp'],
+ 'schedule_end' => [$this, 'formatTimestamp'],
+ 'schedule_start' => [$this, 'formatTimestamp'],
+ 'ttl' => [$this, 'formatSeconds'],
+ 'state' => [$this, 'formatState']
+ ]
+ )
+ ];
+ }
+
+ protected function createRedisInfo(): array
+ {
+ $title = new HtmlElement('h2', null, Text::create(t('Volatile State Details')));
+
+ try {
+ $json = IcingaRedis::instance()->getConnection()
+ ->hGet("icinga:{$this->object->getTableName()}:state", bin2hex($this->object->id));
+ } catch (Exception $e) {
+ return [$title, sprintf('Failed to load redis data: %s', $e->getMessage())];
+ }
+
+ if (! $json) {
+ return [$title, new EmptyState(t('No data available in redis'))];
+ }
+
+ try {
+ $data = Json::decode($json, true);
+ } catch (JsonDecodeException $e) {
+ return [$title, sprintf('Failed to decode redis data: %s', $e->getMessage())];
+ }
+
+ $denylist = [
+ 'commandline',
+ 'environment_id',
+ 'id'
+ ];
+
+ return [$title, $this->createNameValueTable(
+ array_diff_key($data, array_flip($denylist)),
+ [
+ 'last_state_change' => [$this, 'formatMillisecondTimestamp'],
+ 'last_update' => [$this, 'formatMillisecondTimestamp'],
+ 'next_check' => [$this, 'formatMillisecondTimestamp'],
+ 'next_update' => [$this, 'formatMillisecondTimestamp'],
+ 'check_timeout' => [$this, 'formatMilliseconds'],
+ 'execution_time' => [$this, 'formatMilliseconds'],
+ 'latency' => [$this, 'formatMilliseconds'],
+ 'hard_state' => [$this, 'formatState'],
+ 'previous_soft_state' => [$this, 'formatState'],
+ 'previous_hard_state' => [$this, 'formatState'],
+ 'state' => [$this, 'formatState']
+ ]
+ )];
+ }
+
+ protected function createAttributes(): array
+ {
+ $denylist = [
+ 'name',
+ '__name',
+ 'host_name',
+ 'display_name',
+ 'last_check_result',
+ 'source_location',
+ 'templates',
+ 'package',
+ 'version',
+ 'type',
+ 'active',
+ 'paused',
+ 'ha_mode'
+ ];
+
+ return [
+ new HtmlElement('h2', null, Text::create(t('Object Attributes'))),
+ $this->createNameValueTable(
+ array_diff_key($this->attrs, array_flip($denylist)),
+ [
+ 'acknowledgement_expiry' => [$this, 'formatTimestamp'],
+ 'acknowledgement_last_change' => [$this, 'formatTimestamp'],
+ 'check_timeout' => [$this, 'formatSeconds'],
+ 'flapping_last_change' => [$this, 'formatTimestamp'],
+ 'last_check' => [$this, 'formatTimestamp'],
+ 'last_hard_state_change' => [$this, 'formatTimestamp'],
+ 'last_state_change' => [$this, 'formatTimestamp'],
+ 'last_state_ok' => [$this, 'formatTimestamp'],
+ 'last_state_up' => [$this, 'formatTimestamp'],
+ 'last_state_warning' => [$this, 'formatTimestamp'],
+ 'last_state_critical' => [$this, 'formatTimestamp'],
+ 'last_state_down' => [$this, 'formatTimestamp'],
+ 'last_state_unknown' => [$this, 'formatTimestamp'],
+ 'last_state_unreachable' => [$this, 'formatTimestamp'],
+ 'next_check' => [$this, 'formatTimestamp'],
+ 'next_update' => [$this, 'formatTimestamp'],
+ 'previous_state_change' => [$this, 'formatTimestamp'],
+ 'check_interval' => [$this, 'formatSeconds'],
+ 'retry_interval' => [$this, 'formatSeconds'],
+ 'last_hard_state' => [$this, 'formatState'],
+ 'last_state' => [$this, 'formatState'],
+ 'state' => [$this, 'formatState']
+ ]
+ )
+ ];
+ }
+
+ protected function createCustomVariables()
+ {
+ $query = $this->object->customvar
+ ->columns(['name', 'value']);
+
+ $result = [];
+ foreach ($query as $row) {
+ $result[$row->name] = json_decode($row->value, true) ?? $row->value;
+ }
+
+ if (! empty($result)) {
+ $vars = new CustomVarTable($result);
+ } else {
+ $vars = new EmptyState(t('No custom variables configured.'));
+ }
+
+ return [
+ new HtmlElement('h2', null, Text::create(t('Custom Variables'))),
+ $vars
+ ];
+ }
+
+ /**
+ * Format the given value as a json
+ *
+ * @param mixed $json
+ *
+ * @return BaseHtmlElement|string
+ */
+ private function formatJson($json)
+ {
+ if (is_scalar($json)) {
+ return Json::encode($json, JSON_UNESCAPED_SLASHES);
+ }
+
+ return new HtmlElement(
+ 'pre',
+ null,
+ Text::create(Json::encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))
+ );
+ }
+
+ /**
+ * Format the given timestamp
+ *
+ * @param int|float|null $ts
+ *
+ * @return EmptyState|string
+ */
+ private function formatTimestamp($ts)
+ {
+ if (empty($ts)) {
+ return new EmptyState(t('n. a.'));
+ }
+
+ if (is_float($ts)) {
+ $dt = DateTime::createFromFormat('U.u', sprintf('%F', $ts));
+ } else {
+ $dt = (new DateTime())->setTimestamp($ts);
+ }
+
+ return $dt->setTimezone(new DateTimeZone('UTC'))
+ ->format('Y-m-d\TH:i:s.vP');
+ }
+
+ /**
+ * Format the given timestamp (in milliseconds)
+ *
+ * @param int|float|null $ms
+ *
+ * @return EmptyState|string
+ */
+ private function formatMillisecondTimestamp($ms)
+ {
+ return $this->formatTimestamp($ms / 1000.0);
+ }
+
+ private function formatSeconds($s): string
+ {
+ return Format::seconds($s);
+ }
+
+ private function formatMilliseconds($ms): string
+ {
+ return Format::seconds($ms / 1000.0);
+ }
+
+ private function formatState(int $state)
+ {
+ try {
+ switch (true) {
+ case $this->object instanceof Host:
+ return HostStates::text($state);
+ case $this->object instanceof Service:
+ return ServiceStates::text($state);
+ default:
+ return $state;
+ }
+ } catch (InvalidArgumentException $_) {
+ // The Icinga 2 API sometimes delivers strange details
+ return (string) $state;
+ }
+ }
+
+ private function createNameValueTable(array $data, array $formatters): Table
+ {
+ $table = new Table();
+ $table->addAttributes(['class' => 'name-value-table']);
+ foreach ($data as $name => $value) {
+ if (empty($value) && ($value === null || is_string($value) || is_array($value))) {
+ $value = new EmptyState(t('n. a.'));
+ } else {
+ try {
+ if (isset($formatters[$name])) {
+ $value = call_user_func($formatters[$name], $value);
+ } else {
+ $value = $this->formatJson($value);
+
+ if ($value instanceof BaseHtmlElement) {
+ CopyToClipboard::attachTo($value);
+ }
+ }
+ } catch (Exception $e) {
+ $value = new EmptyState(IcingaException::describe($e));
+ }
+ }
+
+ $table->addHtml(Table::tr([
+ Table::th($name),
+ Table::td($value)
+ ]));
+ }
+
+ return $table;
+ }
+}