Skip to content

Commit e741a31

Browse files
committed
Reporting: add first reports
1 parent 1717f30 commit e741a31

File tree

8 files changed

+456
-0
lines changed

8 files changed

+456
-0
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Controllers;
4+
5+
use gipfl\IcingaWeb2\Widget\Tabs;
6+
use Icinga\Authentication\Auth;
7+
use Icinga\Module\Eventtracker\Reporting\HistorySummaries;
8+
use Icinga\Module\Eventtracker\Web\Form\Reporting\AggregationTypeForm;
9+
use Icinga\Module\Eventtracker\Web\Form\Reporting\ReportEndForm;
10+
use Icinga\Module\Eventtracker\Web\Form\Reporting\ReportStartForm;
11+
use Icinga\Module\Eventtracker\Web\Table\Reporting\HistorySummaryTable;
12+
use Icinga\Module\Eventtracker\Web\Widget\AdditionalTableActions;
13+
use Icinga\Web\Url;
14+
15+
class ReportingController extends Controller
16+
{
17+
use RestApiMethods;
18+
19+
protected AggregationTypeForm $formAggregationType;
20+
protected ReportStartForm $formStart;
21+
protected ReportEndForm $formEnd;
22+
23+
public function init()
24+
{
25+
if (! $this->getRequest()->isApiRequest()) {
26+
if (! $this->Auth()->isAuthenticated()) {
27+
$this->redirectToLogin(Url::fromRequest());
28+
}
29+
$this->assertPermission('eventtracker/reporting');
30+
}
31+
$this->formAggregationType = (new AggregationTypeForm())->handleRequest($this->getServerRequest());
32+
$this->formStart = (new ReportStartForm())->handleRequest($this->getServerRequest());
33+
$this->formEnd = (new ReportEndForm())->handleRequest($this->getServerRequest());
34+
}
35+
36+
protected $requiresAuthentication = false;
37+
38+
public function historySummaryAction()
39+
{
40+
if ($this->getRequest()->isApiRequest()) {
41+
$this->runForApi(fn () => $this->sendHistorySummary());
42+
return;
43+
}
44+
if ($this->getParam('format') === 'json') {
45+
// Not using $this->optionallySendJsonForTable($table), as it is indexed differently
46+
$this->sendHistorySummary();
47+
return;
48+
}
49+
50+
$this->addTitle($this->translate('Report: Issue History'));
51+
$this->reportingTabs()->activate('historySummary');
52+
$this->actions()->add([
53+
$this->formAggregationType,
54+
$this->formStart,
55+
$this->formEnd
56+
]);
57+
$table = new HistorySummaryTable($this->db(), $this->requireReport());
58+
$table->getPaginator($this->url())->setItemsPerPage(100);
59+
(new AdditionalTableActions($table, Auth::getInstance(), $this->url()))
60+
->appendTo($this->actions());
61+
$table->renderTo($this);
62+
}
63+
64+
protected function reportingTabs(): Tabs
65+
{
66+
return $this->tabs()->add('historySummary', [
67+
'url' => 'eventtracker/report/history-summary',
68+
'label' => $this->translate('Issue History')
69+
])/*->add('topHosts', [
70+
'url' => 'eventtracker/report/top-hosts',
71+
'label' => $this->translate('Top Hosts')
72+
])->add('topProblemIdentifiers', [
73+
'url' => 'eventtracker/report/top-problem-identifiers',
74+
'label' => $this->translate('Top Problem Identifiers')
75+
])*/;
76+
}
77+
78+
protected function sendHistorySummary()
79+
{
80+
$report = $this->requireReport();
81+
$this->sendJsonResponse($report->fetchIndexed($report->select()));
82+
}
83+
84+
protected function requireReport(): HistorySummaries
85+
{
86+
return new HistorySummaries(
87+
$this->db(),
88+
$this->formAggregationType->getValue('aggregation'),
89+
$this->formStart->getDate(),
90+
$this->formEnd->getDate(),
91+
);
92+
}
93+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Reporting;
4+
5+
use gipfl\Translation\TranslationHelper;
6+
7+
// TODO: enum, once we require PHP 8
8+
class AggregationPeriod
9+
{
10+
use TranslationHelper;
11+
12+
public const HOURLY = 'hourly';
13+
public const DAILY = 'daily';
14+
public const WEEKLY = 'weekly';
15+
public const WEEKDAY = 'weekday';
16+
public const MONTHLY = 'monthly';
17+
18+
public static function enum(): array
19+
{
20+
$t = self::getTranslator();
21+
return [
22+
self::HOURLY => $t->translate('Hourly'),
23+
self::DAILY => $t->translate('Daily'),
24+
self::WEEKLY => $t->translate('Weekly'),
25+
self::WEEKDAY => $t->translate('Day of Week'),
26+
self::MONTHLY => $t->translate('Monthly'),
27+
];
28+
}
29+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Reporting;
4+
5+
use gipfl\Format\LocalDateFormat;
6+
use gipfl\Translation\TranslationHelper;
7+
use RuntimeException;
8+
9+
class AggregationPeriodTitle
10+
{
11+
use TranslationHelper;
12+
13+
protected static ?LocalDateFormat $dateFormatter = null;
14+
protected string $period;
15+
16+
public function __construct(string $aggregation)
17+
{
18+
$this->period = $aggregation;
19+
}
20+
21+
public function getTranslated($key): string
22+
{
23+
switch ($this->period) {
24+
case AggregationPeriod::HOURLY:
25+
return sprintf('%02d:00 - %02d-00', (int) $key - 1, (int) $key);
26+
case AggregationPeriod::WEEKLY:
27+
return sprintf($this->translate('CW %s'), (int) $key);
28+
case AggregationPeriod::WEEKDAY:
29+
return $this->getWeekdayName((int) $key);
30+
case AggregationPeriod::DAILY:
31+
return (self::$dateFormatter ??= new LocalDateFormat())->getFullDay(new \DateTimeImmutable($key));
32+
case AggregationPeriod::MONTHLY:
33+
return $this->getMonthName($key);
34+
default:
35+
throw new RuntimeException("Invalid aggregation: $this->period");
36+
}
37+
}
38+
39+
protected function getWeekdayName(int $key): string
40+
{
41+
switch ($key) {
42+
case 1:
43+
return $this->translate('Monday');
44+
case 2:
45+
return $this->translate('Tuesday');
46+
case 3:
47+
return $this->translate('Wednesday');
48+
case 4:
49+
return $this->translate('Thursday');
50+
case 5:
51+
return $this->translate('Friday');
52+
case 6:
53+
return $this->translate('Saturday');
54+
case 7:
55+
return $this->translate('Sunday');
56+
default:
57+
throw new RuntimeException("Invalid weekday: $key");
58+
}
59+
}
60+
61+
protected function getMonthName(string $key): string
62+
{
63+
$year = substr($key, 0, 4);
64+
$key = (int) substr($key, 4);
65+
switch ($key) {
66+
case 1:
67+
$month = $this->translate('January');
68+
break;
69+
case 2:
70+
$month = $this->translate('February');
71+
break;
72+
case 3:
73+
$month = $this->translate('March');
74+
break;
75+
case 4:
76+
$month = $this->translate('April');
77+
break;
78+
case 5:
79+
$month = $this->translate('May');
80+
break;
81+
case 6:
82+
$month = $this->translate('June');
83+
break;
84+
case 7:
85+
$month = $this->translate('July');
86+
break;
87+
case 8:
88+
$month = $this->translate('August');
89+
break;
90+
case 9:
91+
$month = $this->translate('September');
92+
break;
93+
case 10:
94+
$month = $this->translate('October');
95+
break;
96+
case 11:
97+
$month = $this->translate('November');
98+
break;
99+
case 12:
100+
$month = $this->translate('December');
101+
break;
102+
default:
103+
throw new RuntimeException("Invalid month: $key");
104+
}
105+
106+
return sprintf('%s %s', $month, $year);
107+
}
108+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Reporting;
4+
5+
use DateTimeInterface as DT;
6+
use gipfl\ZfDb\Adapter\Pdo\PdoAdapter;
7+
use gipfl\ZfDb\Select;
8+
use Icinga\Module\Eventtracker\Time;
9+
10+
class HistorySummaries
11+
{
12+
protected PdoAdapter $db;
13+
14+
protected const AGGREGATIONS = [
15+
AggregationPeriod::HOURLY => "RIGHT(CONCAT('0', DATE_FORMAT(FROM_UNIXTIME(FLOOR(ts_first_event / 84000000) * 84000), '%k')), 2)",
16+
AggregationPeriod::DAILY => "DATE_FORMAT(FROM_UNIXTIME(FLOOR(ts_first_event / 84000000) * 84000 + 42000), '%Y-%m-%d')",
17+
AggregationPeriod::WEEKLY => "DATE_FORMAT(FROM_UNIXTIME(FLOOR(ts_first_event / 84000000) * 84000 + 42000), '%u')",
18+
// Hint -> transforming sun(0)-sat(6) into mon(1)-sun(7)
19+
AggregationPeriod::WEEKDAY => "((DATE_FORMAT(FROM_UNIXTIME(FLOOR(ts_first_event / 84000000) * 84000 + 42000), '%w') + 6) % 7 + 1)",
20+
AggregationPeriod::MONTHLY => "DATE_FORMAT(FROM_UNIXTIME(FLOOR(ts_first_event / 84000000) * 84000), '%Y%m')",
21+
];
22+
23+
protected const COLUMNS = [
24+
'cnt_total' => 'COUNT(*)',
25+
'cnt_with_owner' => 'SUM(CASE WHEN owner IS NULL THEN 0 ELSE 1 END)',
26+
'cnt_with_ticket_ref' => 'SUM(CASE WHEN ticket_ref IS NULL THEN 0 ELSE 1 END)',
27+
'cnt_owner_no_ticket_ref' => 'SUM(CASE WHEN ticket_ref IS NULL AND owner IS NOT NULL THEN 1 ELSE 0 END)',
28+
];
29+
protected string $aggregation;
30+
protected DT $start;
31+
protected DT $end;
32+
33+
/**
34+
* @param string $aggregation Should become AggregationPeriod
35+
*/
36+
public function __construct(PdoAdapter $db, string $aggregation, DT $start, DT $end)
37+
{
38+
$this->db = $db;
39+
$this->aggregation = $aggregation;
40+
$this->start = $start;
41+
$this->end = $end;
42+
}
43+
44+
public function getAggregationName(): string
45+
{
46+
return $this->aggregation;
47+
}
48+
49+
public function getAggregationExpression(): string
50+
{
51+
return self::AGGREGATIONS[$this->aggregation];
52+
}
53+
54+
public function getAvailableColumns(): array
55+
{
56+
return self::COLUMNS;
57+
}
58+
59+
public function fetchIndexed(Select $select): array
60+
{
61+
$result = [];
62+
foreach ($this->db->fetchAll($select) as $row) {
63+
$row = (array) $row;
64+
$key = $row['period'];
65+
unset($row['period']);
66+
$result[$key] = array_map('intval', $row);
67+
}
68+
69+
return $result;
70+
}
71+
72+
public function select(): Select
73+
{
74+
$periodExpression = $this->getAggregationExpression();
75+
return $this->db->select()->from('issue_history', ['period' => $periodExpression] + self::COLUMNS)
76+
->where('ts_first_event >= ?', Time::dateTimeToTimestampMs($this->start))
77+
->where('ts_first_event < ?', Time::dateTimeToTimestampMs($this->end))
78+
->order('period')
79+
->group($periodExpression)
80+
;
81+
}
82+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Web\Form\Reporting;
4+
5+
use gipfl\Translation\TranslationHelper;
6+
use gipfl\Web\InlineForm;
7+
use Icinga\Module\Eventtracker\Reporting\AggregationPeriod;
8+
9+
class AggregationTypeForm extends InlineForm
10+
{
11+
use TranslationHelper;
12+
13+
protected $method = 'GET';
14+
protected $useCsrf = false;
15+
protected $useFormName = false;
16+
17+
protected function assemble()
18+
{
19+
$this->addElement('select', 'aggregation', [
20+
'options' => AggregationPeriod::enum(),
21+
'value' => AggregationPeriod::MONTHLY,
22+
'class' => 'autosubmit',
23+
]);
24+
}
25+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Web\Form\Reporting;
4+
5+
use DateTimeImmutable;
6+
use gipfl\Translation\TranslationHelper;
7+
use gipfl\Web\InlineForm;
8+
use InvalidArgumentException;
9+
10+
class ReportEndForm extends InlineForm
11+
{
12+
use TranslationHelper;
13+
14+
protected $useCsrf = false;
15+
protected $useFormName = false;
16+
protected $method = 'GET';
17+
18+
protected function assemble()
19+
{
20+
$this->addElement('date', 'end', [
21+
'class' => 'autosubmit',
22+
'value' => (new DateTimeImmutable())->format('Y-m-d'),
23+
]);
24+
}
25+
26+
public function getDate(): DateTimeImmutable
27+
{
28+
$date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $this->getValue('end') . ' 00:00:00');
29+
if (! $date) {
30+
throw new InvalidArgumentException('Invalid date: ' . $this->getValue('end'));
31+
}
32+
33+
return $date->modify('+1 day');
34+
}
35+
}

0 commit comments

Comments
 (0)