Skip to content

Commit 383acba

Browse files
authored
Alert log modern backend (librenms#18844)
* Alert log modern backend * Extract formatter from controller * Keep filters in url * style fixes * couple fixes * More consistent formatting * Try to prevent database access and add a test * Style fixes * Fix missing device display name * sigh phpstan, this seems decent anyway * fix deprecated calls * Refactor AlertLogDetailFormatter.php * Update tests * Add blade file for formatting and improve visual display * just simply escape alert rule name * Style fixes * rename formatter to parser * Fixup test
1 parent 459b3ed commit 383acba

File tree

16 files changed

+1032
-235
lines changed

16 files changed

+1032
-235
lines changed

LibreNMS/Enum/AlertLogState.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace LibreNMS\Enum;
4+
5+
enum AlertLogState: int
6+
{
7+
case Clear = 0;
8+
case Active = 1;
9+
case Acknowledged = 2;
10+
case Worse = 3;
11+
case Better = 4;
12+
case Changed = 5;
13+
case Recovered = 6;
14+
15+
public function isActive(): bool
16+
{
17+
return match ($this) {
18+
self::Active, self::Worse, self::Better, self::Changed => true,
19+
default => false,
20+
};
21+
}
22+
23+
public function asSeverity(): Severity
24+
{
25+
return match ($this) {
26+
self::Clear, self::Recovered => Severity::Ok,
27+
self::Active => Severity::Error,
28+
self::Acknowledged => Severity::Info,
29+
self::Worse, self::Changed => Severity::Warning,
30+
self::Better => Severity::Notice,
31+
};
32+
}
33+
}

LibreNMS/Util/Html.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ public static function powerStateLabel($state): array
186186
};
187187
}
188188

189-
public static function severityToLabel(Severity $severity, string $text): string
189+
public static function severityToLabel(Severity $severity, string $text = '', string $title = '', string $class = 'label'): string
190190
{
191191
$state_label = match ($severity) {
192192
Severity::Ok => 'label-success',
@@ -197,6 +197,10 @@ public static function severityToLabel(Severity $severity, string $text): string
197197
default => 'label-default',
198198
};
199199

200-
return "<span class=\"label $state_label\">$text</span>";
200+
if ($title) {
201+
$title = " title=\"$title\"";
202+
}
203+
204+
return "<span class=\"$class $state_label\"$title>$text</span>";
201205
}
202206
}

LibreNMS/Util/Url.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ public static function sensorLink($sensor, $text = null, $type = null, $overlib
225225
$text = $label;
226226
}
227227

228-
$content = '<div class=list-large>' . addslashes(htmlentities($sensor->device->displayName() . ' - ' . $label)) . '</div>';
228+
$content = '<div class=list-large>' . addslashes(htmlentities($sensor->device?->displayName() . ' - ' . $label)) . '</div>';
229229

230230
$content .= "<div style=\'width: 850px\'>";
231231
$graph_array = [
@@ -372,6 +372,11 @@ public static function forExternalGraph($args): string
372372
return LaravelUrl::signedRoute('graph', $args);
373373
}
374374

375+
public static function graphPageUrl(string $type, array $args = []): string
376+
{
377+
return url('graphs', ['type' => $type, ...$args]);
378+
}
379+
375380
/**
376381
* @param array $args
377382
* @return string

app/Casts/CompressedJson.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Casts;
4+
5+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class CompressedJson implements CastsAttributes
9+
{
10+
/**
11+
* Cast the given value.
12+
*
13+
* @param array<string, mixed> $attributes
14+
*/
15+
public function get(Model $model, string $key, mixed $value, array $attributes): array
16+
{
17+
return json_decode(@gzuncompress($value), true) ?? [];
18+
}
19+
20+
/**
21+
* Prepare the given value for storage.
22+
*
23+
* @param array<string, mixed> $attributes
24+
*/
25+
public function set(Model $model, string $key, mixed $value, array $attributes): string
26+
{
27+
return gzcompress(json_encode($value));
28+
}
29+
}

app/Http/Controllers/PaginatedAjaxController.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,19 @@ public function formatItem($model)
125125
protected function search($search, $query, $fields)
126126
{
127127
if ($search) {
128-
$query->where(function ($query) use ($fields, $search): void {
129-
foreach ($fields as $field) {
130-
$query->orWhere($field, 'like', '%' . $search . '%');
128+
$query->where(function (Builder $query) use ($fields, $search): void {
129+
foreach ($fields as $index => $field) {
130+
if (! is_numeric($index)) {
131+
$query->orWhereHas($index, function ($query) use ($field, $search): void {
132+
$query->where(function ($query) use ($field, $search): void {
133+
foreach ($field as $relatedField) {
134+
$query->orWhere($relatedField, 'like', '%' . $search . '%');
135+
}
136+
});
137+
});
138+
} else {
139+
$query->orWhere($field, 'like', '%' . $search . '%');
140+
}
131141
}
132142
});
133143
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<?php
2+
3+
namespace App\Http\Controllers\Table;
4+
5+
use App\Http\Parsers\AlertLogDetailParser;
6+
use App\Models\AlertLog;
7+
use Illuminate\Database\Eloquent\Builder;
8+
use Illuminate\Http\Request;
9+
use LibreNMS\Util\Html;
10+
use LibreNMS\Util\Url;
11+
12+
class AlertLogController extends TableController
13+
{
14+
protected $default_sort = ['time_logged' => 'asc'];
15+
16+
public function __construct(
17+
private readonly AlertLogDetailParser $parser
18+
) {
19+
}
20+
21+
protected function rules()
22+
{
23+
return [
24+
'severity' => 'array|nullable',
25+
'severity.*' => 'integer',
26+
'device_id' => 'integer|nullable',
27+
'state' => 'integer|nullable',
28+
];
29+
}
30+
31+
protected function sortFields($request)
32+
{
33+
return [
34+
'time_logged',
35+
'status' => 'state',
36+
'alert_rule' => 'name',
37+
'severity',
38+
'hostname',
39+
];
40+
}
41+
42+
protected function searchFields(Request $request)
43+
{
44+
return [
45+
'device' => ['hostname', 'sysname'],
46+
'rule' => ['name'],
47+
// 'time_logged', // how would this be useful? removed
48+
];
49+
}
50+
51+
protected function filterFields(Request $request)
52+
{
53+
return [
54+
'alert_log.device_id' => 'device_id',
55+
'severity' => function (Builder $q, ?array $severity): void {
56+
if ($severity) {
57+
$q->whereHas('rule', fn ($q) => $q->whereIn('severity', array_map(intval(...), $severity)));
58+
}
59+
},
60+
'state',
61+
];
62+
}
63+
64+
/**
65+
* @inheritDoc
66+
*/
67+
protected function baseQuery(Request $request)
68+
{
69+
$query = AlertLog::query()
70+
->select('alert_log.*')
71+
->with(['device', 'rule'])
72+
->hasAccess($request->user());
73+
74+
$sort = $request->input('sort');
75+
if (isset($sort['severity']) || isset($sort['alert_rule'])) {
76+
$query->leftJoin('alert_rules', 'alert_log.rule_id', '=', 'alert_rules.id');
77+
}
78+
if (isset($sort['hostname'])) {
79+
$query->leftJoin('devices', 'alert_log.device_id', '=', 'devices.device_id');
80+
}
81+
82+
return $query;
83+
}
84+
85+
/**
86+
* Format alert log item for display
87+
*
88+
* @param AlertLog $model
89+
* @return array
90+
*/
91+
public function formatItem($model): array
92+
{
93+
$fault_detail = view('alerts.fault-detail', [
94+
'details' => $this->parser->parse($model->details),
95+
])->render();
96+
97+
$status = Html::severityToLabel($model->state->asSeverity(), title: $model->state->name, class: 'alert-status');
98+
99+
return [
100+
'id' => $model->id,
101+
'time_logged' => $model->time_logged,
102+
'details' => '<a class="fa fa-plus incident-toggle" style="display:none" data-toggle="collapse" data-target="#incident' . $model->id . '" data-parent="#alerts"></a>',
103+
'verbose_details' => "<button type='button' class='btn btn-alert-details verbose-alert-details' style='display:none' aria-label='Details' id='alert-details' data-alert_log_id='$model->id'><i class='fa-solid fa-circle-info'></i></button>",
104+
'hostname' => '<div class="incident">' . Url::modernDeviceLink($model->device) . '<div id="incident' . $model->id . '" class="collapse">' . $fault_detail . '</div></div>',
105+
'alert_rule' => e($model->rule?->name),
106+
'status' => $status,
107+
'severity' => $model->rule?->severity,
108+
];
109+
}
110+
111+
protected function getExportHeaders(): array
112+
{
113+
return [
114+
'id',
115+
'state',
116+
'time_logged',
117+
'device_id',
118+
'device',
119+
'rule_id',
120+
'rule_name',
121+
'rule_severity',
122+
'details',
123+
];
124+
}
125+
126+
protected function formatExportRow($item): array
127+
{
128+
return [
129+
$item->id,
130+
strtolower((string) $item->state->name),
131+
$item->time_logged->toIso8601ZuluString(),
132+
$item->device_id,
133+
$item->device?->displayName(),
134+
$item->rule_id,
135+
$item->rule?->name,
136+
$item->rule?->severity,
137+
json_encode($item->details),
138+
];
139+
}
140+
}

0 commit comments

Comments
 (0)