Skip to content

Commit 644338b

Browse files
committed
Reporting: add top talkers
1 parent 1f16eba commit 644338b

File tree

7 files changed

+253
-24
lines changed

7 files changed

+253
-24
lines changed

application/controllers/ReportingController.php

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
use gipfl\IcingaWeb2\Widget\Tabs;
66
use Icinga\Authentication\Auth;
77
use Icinga\Module\Eventtracker\Reporting\HistorySummaries;
8+
use Icinga\Module\Eventtracker\Reporting\TopTalkers;
9+
use Icinga\Module\Eventtracker\Web\Form\Reporting\AggregationSubjectForm;
810
use Icinga\Module\Eventtracker\Web\Form\Reporting\AggregationTypeForm;
911
use Icinga\Module\Eventtracker\Web\Form\Reporting\ReportEndForm;
1012
use Icinga\Module\Eventtracker\Web\Form\Reporting\ReportStartForm;
13+
use Icinga\Module\Eventtracker\Web\Table\BaseTable;
1114
use Icinga\Module\Eventtracker\Web\Table\Reporting\HistorySummaryTable;
15+
use Icinga\Module\Eventtracker\Web\Table\Reporting\TopTalkersTable;
1216
use Icinga\Module\Eventtracker\Web\Widget\AdditionalTableActions;
1317
use Icinga\Web\Url;
1418

@@ -17,6 +21,7 @@ class ReportingController extends Controller
1721
use RestApiMethods;
1822

1923
protected AggregationTypeForm $formAggregationType;
24+
protected AggregationSubjectForm $formAggregationSubject;
2025
protected ReportStartForm $formStart;
2126
protected ReportEndForm $formEnd;
2227

@@ -29,6 +34,7 @@ public function init()
2934
$this->assertPermission('eventtracker/reporting');
3035
}
3136
$this->formAggregationType = (new AggregationTypeForm())->handleRequest($this->getServerRequest());
37+
$this->formAggregationSubject = (new AggregationSubjectForm())->handleRequest($this->getServerRequest());
3238
$this->formStart = (new ReportStartForm())->handleRequest($this->getServerRequest());
3339
$this->formEnd = (new ReportEndForm())->handleRequest($this->getServerRequest());
3440
}
@@ -54,34 +60,52 @@ public function historySummaryAction()
5460
$this->formStart,
5561
$this->formEnd
5662
]);
57-
$table = new HistorySummaryTable($this->db(), $this->requireReport());
63+
$this->showTable(new HistorySummaryTable($this->db(), $this->requireHistorySummaryReport()));
64+
}
65+
66+
public function topTalkersAction()
67+
{
68+
if ($this->getRequest()->isApiRequest()) {
69+
$this->runForApi(fn () => $this->sendTopTalkers());
70+
return;
71+
}
72+
if ($this->getParam('format') === 'json') {
73+
// Not using $this->optionallySendJsonForTable($table), as it is indexed differently
74+
$this->sendTopTalkers();
75+
return;
76+
}
77+
78+
$this->addTitle($this->translate('Report: Top Talkers'));
79+
$this->reportingTabs()->activate('topTalkers');
80+
$this->actions()->add([
81+
$this->formAggregationSubject,
82+
$this->formStart,
83+
$this->formEnd
84+
]);
85+
$this->showTable(new TopTalkersTable($this->db(), $this->requireTopTalkersReport()));
86+
}
87+
88+
protected function showTable(BaseTable $table)
89+
{
5890
$table->getPaginator($this->url())->setItemsPerPage(100);
5991
(new AdditionalTableActions($table, Auth::getInstance(), $this->url()))
6092
->appendTo($this->actions());
6193
$table->renderTo($this);
6294
}
6395

64-
protected function reportingTabs(): Tabs
96+
protected function sendHistorySummary()
6597
{
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-
])*/;
98+
$report = $this->requireHistorySummaryReport();
99+
$this->sendJsonResponse(['objects' => $report->fetchIndexed($report->select())]);
76100
}
77101

78-
protected function sendHistorySummary()
102+
protected function sendTopTalkers()
79103
{
80-
$report = $this->requireReport();
104+
$report = $this->requireTopTalkersReport();
81105
$this->sendJsonResponse(['objects' => $report->fetchIndexed($report->select())]);
82106
}
83107

84-
protected function requireReport(): HistorySummaries
108+
protected function requireHistorySummaryReport(): HistorySummaries
85109
{
86110
return new HistorySummaries(
87111
$this->db(),
@@ -90,4 +114,25 @@ protected function requireReport(): HistorySummaries
90114
$this->formEnd->getDate(),
91115
);
92116
}
117+
118+
protected function requireTopTalkersReport(): TopTalkers
119+
{
120+
return new TopTalkers(
121+
$this->db(),
122+
$this->formAggregationSubject->getValue('aggregation'),
123+
$this->formStart->getDate(),
124+
$this->formEnd->getDate(),
125+
);
126+
}
127+
128+
protected function reportingTabs(): Tabs
129+
{
130+
return $this->tabs()->add('historySummary', [
131+
'url' => 'eventtracker/report/history-summary',
132+
'label' => $this->translate('Issue History')
133+
])->add('topTalkers', [
134+
'url' => 'eventtracker/reporting/top-talkers',
135+
'label' => $this->translate('Top Talkers')
136+
]);
137+
}
93138
}

doc/61-REST_API.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,25 @@ Accept: application/json
723723

724724
The following parameters are optional:
725725

726-
| Parameter | Description |
727-
|-------------|---------------------------------------------------------------------------------|
728-
| aggregation | Aggregation type (hourly, daily, weekly, weekday, monthly |
729-
| start | Day on which your report should start (YYYY-mm-dd, defaults to "now - 1 month") |
730-
| end | Day on which your report should end (YYYY-mm-dd, defaults to "now") |
726+
| Parameter | Description |
727+
|-------------|------------------------------------------------------------------------------------------|
728+
| aggregation | Aggregation type: hourly (default), daily, weekly, weekday, monthly, defaults to "daily" |
729+
| start | Day on which your report should start: YYYY-mm-dd, defaults to "now - 1 month" |
730+
| end | Day on which your report should end: YYYY-mm-dd, defaults to "now" |
731+
732+
733+
### Fetch Report: Top Talkers
734+
735+
```http
736+
GET https://monitoring.example.com/icingaweb2/eventtracker/reporting/top-talkers?start=2025-11-10&end=2025-11-12&aggregation=host
737+
Authorization: Bearer 108600bf-4f77-4bdc-9a06-4cd04902537c4
738+
Accept: application/json
739+
```
740+
741+
Same as the above, but the aggregation is based on the object type. The followi
742+
743+
| Parameter | Description |
744+
|-------------|--------------------------------------------------------------------------------------|
745+
| aggregation | Aggregation type: host_name (default), object_name, object_class, problem_identifier |
746+
| start | Day on which your report should start: YYYY-mm-dd, defaults to "now - 1 month" |
747+
| end | Day on which your report should end: YYYY-mm-dd, defaults to "now" |

library/Eventtracker/Reporting/AggregationPeriodTitle.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ class AggregationPeriodTitle
1111
use TranslationHelper;
1212

1313
protected static ?LocalDateFormat $dateFormatter = null;
14-
protected string $period;
14+
protected string $aggregation;
1515

1616
public function __construct(string $aggregation)
1717
{
18-
$this->period = $aggregation;
18+
$this->aggregation = $aggregation;
1919
}
2020

2121
public function getTranslated($key): string
2222
{
23-
switch ($this->period) {
23+
switch ($this->aggregation) {
2424
case AggregationPeriod::HOURLY:
2525
return sprintf('%02d:00 - %02d-00', (int) $key - 1, (int) $key);
2626
case AggregationPeriod::WEEKLY:
@@ -32,7 +32,7 @@ public function getTranslated($key): string
3232
case AggregationPeriod::MONTHLY:
3333
return $this->getMonthName($key);
3434
default:
35-
throw new RuntimeException("Invalid aggregation: $this->period");
35+
throw new RuntimeException("Invalid aggregation: $this->aggregation");
3636
}
3737
}
3838

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 AggregationSubject
9+
{
10+
use TranslationHelper;
11+
12+
public const HOST = 'host_name';
13+
public const OBJECT = 'object_name';
14+
public const OBJECT_CLASS = 'object_class';
15+
public const PROBLEM_IDENTIFIER = 'problem_identifier';
16+
17+
public static function enum(): array
18+
{
19+
$t = self::getTranslator();
20+
return [
21+
self::HOST => $t->translate('Host'),
22+
self::OBJECT => $t->translate('Object'),
23+
self::OBJECT_CLASS => $t->translate('Object Class'),
24+
self::PROBLEM_IDENTIFIER => $t->translate('Problem Identifier'),
25+
];
26+
}
27+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 TopTalkers
11+
{
12+
protected PdoAdapter $db;
13+
14+
protected const AGGREGATIONS = [
15+
AggregationSubject::HOST => 'host_name',
16+
AggregationSubject::OBJECT => 'object_name',
17+
AggregationSubject::OBJECT_CLASS => 'object_class',
18+
AggregationSubject::PROBLEM_IDENTIFIER => 'problem_identifier',
19+
];
20+
21+
protected const COLUMNS = [
22+
'cnt_total' => 'COUNT(*)',
23+
];
24+
protected string $aggregation;
25+
protected DT $start;
26+
protected DT $end;
27+
28+
/**
29+
* @param string $aggregation Should become AggregationSubject
30+
*/
31+
public function __construct(PdoAdapter $db, string $aggregation, DT $start, DT $end)
32+
{
33+
$this->db = $db;
34+
$this->aggregation = $aggregation;
35+
$this->start = $start;
36+
$this->end = $end;
37+
}
38+
39+
public function getAggregationName(): string
40+
{
41+
return $this->aggregation;
42+
}
43+
44+
public function getAggregationExpression(): string
45+
{
46+
return self::AGGREGATIONS[$this->aggregation];
47+
}
48+
49+
public function getAvailableColumns(): array
50+
{
51+
return self::COLUMNS;
52+
}
53+
54+
public function fetchIndexed(Select $select): array
55+
{
56+
$result = [];
57+
foreach ($this->db->fetchAll($select) as $row) {
58+
$row = (array) $row;
59+
$key = $row['aggregation'];
60+
unset($row['aggregation']);
61+
$result[$key] = array_map('intval', $row);
62+
}
63+
64+
return $result;
65+
}
66+
67+
public function select(): Select
68+
{
69+
$subject = $this->getAggregationExpression();
70+
return $this->db->select()->from('issue_history', ['aggregation' => $subject] + self::COLUMNS)
71+
->where('ts_first_event >= ?', Time::dateTimeToTimestampMs($this->start))
72+
->where('ts_first_event < ?', Time::dateTimeToTimestampMs($this->end))
73+
->order('cnt_total DESC')
74+
->group($subject)
75+
->limit(50)
76+
;
77+
}
78+
}
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\AggregationSubject;
8+
9+
class AggregationSubjectForm 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' => AggregationSubject::enum(),
21+
'value' => AggregationSubject::HOST,
22+
'class' => 'autosubmit',
23+
]);
24+
}
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Icinga\Module\Eventtracker\Web\Table\Reporting;
4+
5+
use gipfl\ZfDb\Adapter\Pdo\PdoAdapter;
6+
use gipfl\ZfDb\Select;
7+
use Icinga\Module\Eventtracker\Reporting\TopTalkers;
8+
use Icinga\Module\Eventtracker\Web\Table\BaseTable;
9+
10+
class TopTalkersTable extends BaseTable
11+
{
12+
protected TopTalkers $report;
13+
14+
public function __construct(PdoAdapter $db, TopTalkers $report)
15+
{
16+
parent::__construct($db);
17+
$this->report = $report;
18+
}
19+
20+
public function initialize()
21+
{
22+
$expressions = $this->report->getAvailableColumns();
23+
$this->addAvailableColumns([
24+
$this->createColumn('aggregation', $this->translate('Subject'), [
25+
'aggregation' => $this->report->getAggregationExpression(),
26+
]),
27+
$this->createColumn('cnt_total', $this->translate('Issues'), [
28+
'cnt_total' => $expressions['cnt_total'],
29+
]),
30+
]);
31+
}
32+
33+
protected function prepareQuery(): Select
34+
{
35+
return $this->report->select();
36+
}
37+
}

0 commit comments

Comments
 (0)