diff --git a/src/Actions/Dto/GetTrendResponseDto.php b/src/Actions/Dto/GetTrendResponseDto.php new file mode 100644 index 0000000..2ffc738 --- /dev/null +++ b/src/Actions/Dto/GetTrendResponseDto.php @@ -0,0 +1,96 @@ + $items + */ + public function __construct( + public array $items = [], + ) { + } + + /** + * @param array> $data + */ + public static function fromArray(array $data): self + { + $items = []; + foreach ($data as $item) { + if (!is_array($item)) { + continue; + } + $items[] = TrendItemDto::fromArray($item); + } + + return new self($items); + } + + public function isEmpty(): bool + { + return $this->items === []; + } + + public function count(): int + { + return count($this->items); + } + + /** + * @return list + */ + public function getValues(): array + { + return array_map(static fn (TrendItemDto $item) => $item->valueAvg, $this->items); + } + + /** + * @return list + */ + public function getMinValues(): array + { + return array_map(static fn (TrendItemDto $item) => $item->valueMin, $this->items); + } + + /** + * @return list + */ + public function getMaxValues(): array + { + return array_map(static fn (TrendItemDto $item) => $item->valueMax, $this->items); + } + + public function getAverageValue(): float + { + $values = $this->getValues(); + if ($values === []) { + return 0.0; + } + + return array_sum($values) / count($values); + } + + public function getMinValue(): float + { + $values = $this->getMinValues(); + if ($values === []) { + return 0.0; + } + + return min($values); + } + + public function getMaxValue(): float + { + $values = $this->getMaxValues(); + if ($values === []) { + return 0.0; + } + + return max($values); + } +} \ No newline at end of file diff --git a/src/Actions/Dto/TrendItemDto.php b/src/Actions/Dto/TrendItemDto.php new file mode 100644 index 0000000..b7c5e62 --- /dev/null +++ b/src/Actions/Dto/TrendItemDto.php @@ -0,0 +1,45 @@ + $data + */ + public static function fromArray(array $data): self + { + $itemid = $data['itemid'] ?? ''; + $clock = $data['clock'] ?? 0; + $num = $data['num'] ?? 0; + $valueMin = $data['value_min'] ?? 0; + $valueAvg = $data['value_avg'] ?? 0; + $valueMax = $data['value_max'] ?? 0; + + return new self( + itemid: is_string($itemid) ? $itemid : '', + clock: is_int($clock) ? $clock : 0, + num: is_int($num) ? $num : 0, + valueMin: is_numeric($valueMin) ? (float) $valueMin : 0.0, + valueAvg: is_numeric($valueAvg) ? (float) $valueAvg : 0.0, + valueMax: is_numeric($valueMax) ? (float) $valueMax : 0.0, + ); + } + + public function getTimestamp(): \DateTimeImmutable + { + return (new \DateTimeImmutable())->setTimestamp($this->clock); + } +} \ No newline at end of file diff --git a/src/Actions/Trend.php b/src/Actions/Trend.php new file mode 100644 index 0000000..5037e15 --- /dev/null +++ b/src/Actions/Trend.php @@ -0,0 +1,191 @@ + $itemIds + * @param array $additionalParams + */ + public function get( + array $itemIds, + ?int $timeFrom = null, + ?int $timeTill = null, + ?int $limit = null, + string $sortField = 'clock', + string $sortOrder = 'DESC', + bool $preserveKeys = false, + array $additionalParams = [], + ): GetTrendResponseDto { + if ($itemIds === []) { + return new GetTrendResponseDto([]); + } + + $params = [ + 'output' => OutputEnum::EXTEND->value, + 'itemids' => $itemIds, + 'sortfield' => $sortField, + 'sortorder' => $sortOrder, + 'preservekeys' => $preserveKeys, + ...$additionalParams, + ]; + + if ($timeFrom !== null) { + $params['time_from'] = $timeFrom; + } + + if ($timeTill !== null) { + $params['time_till'] = $timeTill; + } + + if ($limit !== null) { + $params['limit'] = $limit; + } + + $result = $this->client->call(ZabbixAction::TREND_GET, $params); + + /** @var array> $trendData */ + $trendData = is_array($result) ? $result : []; + + return GetTrendResponseDto::fromArray($trendData); + } + + /** + * @param list $itemIds + */ + public function getLast24Hours( + array $itemIds, + ?int $limit = null, + ): GetTrendResponseDto { + $now = time(); + $twentyFourHoursAgo = $now - 86400; + + return $this->get( + itemIds: $itemIds, + timeFrom: $twentyFourHoursAgo, + timeTill: $now, + limit: $limit, + ); + } + + /** + * @param list $itemIds + */ + public function getLast7Days( + array $itemIds, + ?int $limit = null, + ): GetTrendResponseDto { + $now = time(); + $sevenDaysAgo = $now - (7 * 86400); + + return $this->get( + itemIds: $itemIds, + timeFrom: $sevenDaysAgo, + timeTill: $now, + limit: $limit, + ); + } + + /** + * @param list $itemIds + */ + public function getLast30Days( + array $itemIds, + ?int $limit = null, + ): GetTrendResponseDto { + $now = time(); + $thirtyDaysAgo = $now - (30 * 86400); + + return $this->get( + itemIds: $itemIds, + timeFrom: $thirtyDaysAgo, + timeTill: $now, + limit: $limit, + ); + } + + /** + * @param list $itemIds + * @param array $filter + */ + public function getWithFilter( + array $itemIds, + array $filter, + ?int $timeFrom = null, + ?int $timeTill = null, + ?int $limit = null, + ): GetTrendResponseDto { + if ($itemIds === []) { + return new GetTrendResponseDto([]); + } + + $params = [ + 'output' => OutputEnum::EXTEND->value, + 'itemids' => $itemIds, + 'filter' => $filter, + 'sortfield' => 'clock', + 'sortorder' => 'DESC', + ]; + + if ($timeFrom !== null) { + $params['time_from'] = $timeFrom; + } + + if ($timeTill !== null) { + $params['time_till'] = $timeTill; + } + + if ($limit !== null) { + $params['limit'] = $limit; + } + + $result = $this->client->call(ZabbixAction::TREND_GET, $params); + + /** @var array> $trendData */ + $trendData = is_array($result) ? $result : []; + + return GetTrendResponseDto::fromArray($trendData); + } + + /** + * @param list $itemIds + */ + public function count( + array $itemIds, + ?int $timeFrom = null, + ?int $timeTill = null, + ): int { + if ($itemIds === []) { + return 0; + } + + $params = [ + 'countOutput' => true, + 'itemids' => $itemIds, + ]; + + if ($timeFrom !== null) { + $params['time_from'] = $timeFrom; + } + + if ($timeTill !== null) { + $params['time_till'] = $timeTill; + } + + $result = $this->client->call(ZabbixAction::TREND_GET, $params); + + return is_numeric($result) ? (int) $result : 0; + } +} \ No newline at end of file diff --git a/src/Enums/ZabbixAction.php b/src/Enums/ZabbixAction.php index 01312fc..6c78989 100644 --- a/src/Enums/ZabbixAction.php +++ b/src/Enums/ZabbixAction.php @@ -47,6 +47,7 @@ enum ZabbixAction: string case ITEM_DELETE = 'item.delete'; case ITEM_GET = 'item.get'; case ITEM_UPDATE = 'item.update'; + case TREND_GET = 'trend.get'; case TRIGGER_CREATE = 'trigger.create'; case TRIGGER_DELETE = 'trigger.delete'; case TRIGGER_GET = 'trigger.get'; diff --git a/src/Service/ZabbixItemRegistry.php b/src/Service/ZabbixItemRegistry.php index 87e82b6..6fbd441 100644 --- a/src/Service/ZabbixItemRegistry.php +++ b/src/Service/ZabbixItemRegistry.php @@ -72,6 +72,7 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 0, 'history' => '7d', + 'trends' => '365d', 'units' => 'ms', ], 'tx.http_status' => [ @@ -79,12 +80,14 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'tx.error_rate' => [ 'name' => 'HTTP Error Rate', 'type' => 2, 'value_type' => 0, 'history' => '7d', + 'trends' => '365d', 'units' => '%', ], 'auth.login.success' => [ @@ -92,12 +95,14 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'auth.login.failure' => [ 'name' => 'Login Failure Count', 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'auth.login.success_event' => [ 'name' => 'Login Success Event', @@ -116,18 +121,21 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'entity.update.success' => [ 'name' => 'Entity Update Count', 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'entity.remove.success' => [ 'name' => 'Entity Remove Count', 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'error.exception' => [ 'name' => 'Exception Event', @@ -140,18 +148,21 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'messenger.failed.count' => [ 'name' => 'Failed Messages Count', 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'messenger.processing_ms' => [ 'name' => 'Message Processing Time', 'type' => 2, 'value_type' => 0, 'history' => '7d', + 'trends' => '365d', 'units' => 'ms', ], 'messenger.received' => [ @@ -159,18 +170,21 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'messenger.handled' => [ 'name' => 'Messages Handled', 'type' => 2, 'value_type' => 3, 'history' => '7d', + 'trends' => '365d', ], 'cache.hit_rate' => [ 'name' => 'Cache Hit Rate', 'type' => 2, 'value_type' => 0, 'history' => '7d', + 'trends' => '365d', 'units' => '%', ], 'db.query_time_ms' => [ @@ -178,6 +192,7 @@ public function getAllItemDefinitions(): array 'type' => 2, 'value_type' => 0, 'history' => '7d', + 'trends' => '365d', 'units' => 'ms', ], ]; diff --git a/src/Setup/ZabbixSetup.php b/src/Setup/ZabbixSetup.php index eae9e13..9a8a4ac 100644 --- a/src/Setup/ZabbixSetup.php +++ b/src/Setup/ZabbixSetup.php @@ -178,6 +178,10 @@ private function ensureItems(string $hostId): void 'history' => $definition['history'], ]; + if (isset($definition['trends'])) { + $itemData['trends'] = $definition['trends']; + } + if (isset($definition['units'])) { $itemData['units'] = $definition['units']; } diff --git a/tests/Actions/Dto/GetTrendResponseDtoTest.php b/tests/Actions/Dto/GetTrendResponseDtoTest.php new file mode 100644 index 0000000..4b046e0 --- /dev/null +++ b/tests/Actions/Dto/GetTrendResponseDtoTest.php @@ -0,0 +1,148 @@ +assertTrue($dto->isEmpty()); + $this->assertCount(0, $dto->items); + $this->assertSame(0, $dto->count()); + } + + public function testFromArrayCreatesItems(): void + { + $data = [ + [ + 'itemid' => '123', + 'clock' => 1709500000, + 'num' => 60, + 'value_min' => '10.5', + 'value_avg' => '25.3', + 'value_max' => '50.0', + ], + [ + 'itemid' => '456', + 'clock' => 1709500100, + 'num' => 120, + 'value_min' => '5.0', + 'value_avg' => '15.0', + 'value_max' => '30.0', + ], + ]; + + $dto = GetTrendResponseDto::fromArray($data); + + $this->assertFalse($dto->isEmpty()); + $this->assertCount(2, $dto->items); + $this->assertSame(2, $dto->count()); + } + + public function testGetValuesReturnsAverageValues(): void + { + $items = [ + new TrendItemDto('1', 1709500000, 60, 10.0, 25.0, 50.0), + new TrendItemDto('2', 1709500100, 120, 5.0, 15.0, 30.0), + ]; + + $dto = new GetTrendResponseDto($items); + + $values = $dto->getValues(); + + $this->assertSame([25.0, 15.0], $values); + } + + public function testGetMinValuesReturnsMinValues(): void + { + $items = [ + new TrendItemDto('1', 1709500000, 60, 10.0, 25.0, 50.0), + new TrendItemDto('2', 1709500100, 120, 5.0, 15.0, 30.0), + ]; + + $dto = new GetTrendResponseDto($items); + + $values = $dto->getMinValues(); + + $this->assertSame([10.0, 5.0], $values); + } + + public function testGetMaxValuesReturnsMaxValues(): void + { + $items = [ + new TrendItemDto('1', 1709500000, 60, 10.0, 25.0, 50.0), + new TrendItemDto('2', 1709500100, 120, 5.0, 15.0, 30.0), + ]; + + $dto = new GetTrendResponseDto($items); + + $values = $dto->getMaxValues(); + + $this->assertSame([50.0, 30.0], $values); + } + + public function testGetAverageValueReturnsZeroForEmptyItems(): void + { + $dto = new GetTrendResponseDto([]); + + $this->assertSame(0.0, $dto->getAverageValue()); + } + + public function testGetAverageValueCalculatesCorrectly(): void + { + $items = [ + new TrendItemDto('1', 1709500000, 60, 10.0, 20.0, 50.0), + new TrendItemDto('2', 1709500100, 120, 5.0, 40.0, 30.0), + ]; + + $dto = new GetTrendResponseDto($items); + + $this->assertSame(30.0, $dto->getAverageValue()); + } + + public function testGetMinValueReturnsZeroForEmptyItems(): void + { + $dto = new GetTrendResponseDto([]); + + $this->assertSame(0.0, $dto->getMinValue()); + } + + public function testGetMinValueReturnsMinimum(): void + { + $items = [ + new TrendItemDto('1', 1709500000, 60, 10.0, 25.0, 50.0), + new TrendItemDto('2', 1709500100, 120, 5.0, 15.0, 30.0), + ]; + + $dto = new GetTrendResponseDto($items); + + $this->assertSame(5.0, $dto->getMinValue()); + } + + public function testGetMaxValueReturnsZeroForEmptyItems(): void + { + $dto = new GetTrendResponseDto([]); + + $this->assertSame(0.0, $dto->getMaxValue()); + } + + public function testGetMaxValueReturnsMaximum(): void + { + $items = [ + new TrendItemDto('1', 1709500000, 60, 10.0, 25.0, 50.0), + new TrendItemDto('2', 1709500100, 120, 5.0, 15.0, 30.0), + ]; + + $dto = new GetTrendResponseDto($items); + + $this->assertSame(50.0, $dto->getMaxValue()); + } +} \ No newline at end of file diff --git a/tests/Actions/Dto/TrendItemDtoTest.php b/tests/Actions/Dto/TrendItemDtoTest.php new file mode 100644 index 0000000..beca913 --- /dev/null +++ b/tests/Actions/Dto/TrendItemDtoTest.php @@ -0,0 +1,64 @@ + '12345', + 'clock' => 1709500000, + 'num' => 60, + 'value_min' => '10.5', + 'value_avg' => '25.3', + 'value_max' => '50.0', + ]; + + $dto = TrendItemDto::fromArray($data); + + $this->assertSame('12345', $dto->itemid); + $this->assertSame(1709500000, $dto->clock); + $this->assertSame(60, $dto->num); + $this->assertSame(10.5, $dto->valueMin); + $this->assertSame(25.3, $dto->valueAvg); + $this->assertSame(50.0, $dto->valueMax); + } + + public function testFromArrayHandlesMissingData(): void + { + $data = []; + + $dto = TrendItemDto::fromArray($data); + + $this->assertSame('', $dto->itemid); + $this->assertSame(0, $dto->clock); + $this->assertSame(0, $dto->num); + $this->assertSame(0.0, $dto->valueMin); + $this->assertSame(0.0, $dto->valueAvg); + $this->assertSame(0.0, $dto->valueMax); + } + + public function testGetTimestampReturnsDateTimeImmutable(): void + { + $data = [ + 'itemid' => '12345', + 'clock' => 1709500000, + 'num' => 60, + 'value_min' => '10.5', + 'value_avg' => '25.3', + 'value_max' => '50.0', + ]; + + $dto = TrendItemDto::fromArray($data); + $timestamp = $dto->getTimestamp(); + + $this->assertInstanceOf(\DateTimeImmutable::class, $timestamp); + $this->assertSame(1709500000, $timestamp->getTimestamp()); + } +} \ No newline at end of file diff --git a/tests/Actions/TrendTest.php b/tests/Actions/TrendTest.php new file mode 100644 index 0000000..2e129c7 --- /dev/null +++ b/tests/Actions/TrendTest.php @@ -0,0 +1,180 @@ +client = $this->createMock(ZabbixClientInterface::class); + $this->trend = new Trend($this->client); + } + + public function testGetActionPrefix(): void + { + $this->assertSame('trend', Trend::getActionPrefix()); + } + + public function testGetReturnsEmptyResponseForEmptyItemIds(): void + { + $result = $this->trend->get([]); + + $this->assertTrue($result->isEmpty()); + $this->assertCount(0, $result->items); + } + + public function testGetCallsTrendGetAction(): void + { + $this->client->expects($this->once()) + ->method('call') + ->with( + ZabbixAction::TREND_GET, + $this->callback(static fn (array $params) => $params['itemids'] === ['123', '456']) + ) + ->willReturn([ + [ + 'itemid' => '123', + 'clock' => 1709500000, + 'num' => 60, + 'value_min' => '10.5', + 'value_avg' => '25.3', + 'value_max' => '50.0', + ], + ]); + + $result = $this->trend->get(['123', '456']); + + $this->assertCount(1, $result->items); + $this->assertSame('123', $result->items[0]->itemid); + $this->assertSame(1709500000, $result->items[0]->clock); + $this->assertSame(60, $result->items[0]->num); + $this->assertSame(10.5, $result->items[0]->valueMin); + $this->assertSame(25.3, $result->items[0]->valueAvg); + $this->assertSame(50.0, $result->items[0]->valueMax); + } + + public function testGetWithTimeRange(): void + { + $timeFrom = 1709400000; + $timeTill = 1709500000; + + $this->client->expects($this->once()) + ->method('call') + ->with( + ZabbixAction::TREND_GET, + $this->callback(static fn (array $params) => + $params['time_from'] === $timeFrom + && $params['time_till'] === $timeTill + ) + ) + ->willReturn([]); + + $result = $this->trend->get(['123'], timeFrom: $timeFrom, timeTill: $timeTill); + + $this->assertTrue($result->isEmpty()); + } + + public function testGetLast24Hours(): void + { + $this->client->expects($this->once()) + ->method('call') + ->with( + ZabbixAction::TREND_GET, + $this->callback(static fn (array $params) => + isset($params['time_from']) + && isset($params['time_till']) + && $params['time_till'] - $params['time_from'] === 86400 + ) + ) + ->willReturn([]); + + $result = $this->trend->getLast24Hours(['123']); + + $this->assertTrue($result->isEmpty()); + } + + public function testGetLast7Days(): void + { + $this->client->expects($this->once()) + ->method('call') + ->with( + ZabbixAction::TREND_GET, + $this->callback(static fn (array $params) => + isset($params['time_from']) + && isset($params['time_till']) + && $params['time_till'] - $params['time_from'] === 7 * 86400 + ) + ) + ->willReturn([]); + + $result = $this->trend->getLast7Days(['123']); + + $this->assertTrue($result->isEmpty()); + } + + public function testGetLast30Days(): void + { + $this->client->expects($this->once()) + ->method('call') + ->with( + ZabbixAction::TREND_GET, + $this->callback(static fn (array $params) => + isset($params['time_from']) + && isset($params['time_till']) + && $params['time_till'] - $params['time_from'] === 30 * 86400 + ) + ) + ->willReturn([]); + + $result = $this->trend->getLast30Days(['123']); + + $this->assertTrue($result->isEmpty()); + } + + public function testCountReturnsZeroForEmptyItemIds(): void + { + $result = $this->trend->count([]); + + $this->assertSame(0, $result); + } + + public function testCountReturnsCountFromResponse(): void + { + $this->client->expects($this->once()) + ->method('call') + ->with( + ZabbixAction::TREND_GET, + $this->callback(static fn (array $params) => $params['countOutput'] === true) + ) + ->willReturn(42); + + $result = $this->trend->count(['123']); + + $this->assertSame(42, $result); + } + + public function testCountReturnsZeroForNonNumericResponse(): void + { + $this->client->expects($this->once()) + ->method('call') + ->willReturn(null); + + $result = $this->trend->count(['123']); + + $this->assertSame(0, $result); + } +} \ No newline at end of file