Skip to content

Commit ee3c54f

Browse files
authored
feat: add metrics endpoint for load balancers and servers (#331)
Adds the missing metrics endpoint for the load balancers and servers resources. - https://docs.hetzner.cloud/#load-balancers-get-metrics-for-a-loadbalancer - https://docs.hetzner.cloud/#servers-get-metrics-for-a-server
1 parent b46df8c commit ee3c54f

13 files changed

Lines changed: 391 additions & 2 deletions

File tree

examples/get_server_metrics.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from datetime import datetime, timedelta
5+
from os import environ
6+
7+
from hcloud import Client
8+
from hcloud.images import Image
9+
from hcloud.server_types import ServerType
10+
11+
assert (
12+
"HCLOUD_TOKEN" in environ
13+
), "Please export your API token in the HCLOUD_TOKEN environment variable"
14+
token = environ["HCLOUD_TOKEN"]
15+
16+
client = Client(token=token)
17+
18+
server = client.servers.get_by_name("my-server")
19+
if server is None:
20+
response = client.servers.create(
21+
name="my-server",
22+
server_type=ServerType("cx11"),
23+
image=Image(name="ubuntu-22.04"),
24+
)
25+
server = response.server
26+
27+
end = datetime.now()
28+
start = end - timedelta(hours=1)
29+
30+
response = server.get_metrics(
31+
type=["cpu", "network"],
32+
start=start,
33+
end=end,
34+
)
35+
36+
print(json.dumps(response.metrics))

hcloud/load_balancers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
)
88
from .domain import ( # noqa: F401
99
CreateLoadBalancerResponse,
10+
GetMetricsResponse,
1011
IPv4Address,
1112
IPv6Network,
1213
LoadBalancer,

hcloud/load_balancers/client.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
from __future__ import annotations
22

3+
from datetime import datetime
34
from typing import TYPE_CHECKING, Any, NamedTuple
45

6+
from dateutil.parser import isoparse
7+
58
from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient
69
from ..certificates import BoundCertificate
710
from ..core import BoundModelBase, ClientEntityBase, Meta
811
from ..load_balancer_types import BoundLoadBalancerType
912
from ..locations import BoundLocation
13+
from ..metrics import Metrics
1014
from ..networks import BoundNetwork
1115
from ..servers import BoundServer
1216
from .domain import (
1317
CreateLoadBalancerResponse,
18+
GetMetricsResponse,
1419
IPv4Address,
1520
IPv6Network,
1621
LoadBalancer,
@@ -23,6 +28,7 @@
2328
LoadBalancerTargetHealthStatus,
2429
LoadBalancerTargetIP,
2530
LoadBalancerTargetLabelSelector,
31+
MetricsType,
2632
PrivateNet,
2733
PublicNetwork,
2834
)
@@ -177,6 +183,28 @@ def delete(self) -> bool:
177183
"""
178184
return self._client.delete(self)
179185

186+
def get_metrics(
187+
self,
188+
type: MetricsType,
189+
start: datetime | str,
190+
end: datetime | str,
191+
step: float | None = None,
192+
) -> GetMetricsResponse:
193+
"""Get Metrics for a LoadBalancer.
194+
195+
:param type: Type of metrics to get.
196+
:param start: Start of period to get Metrics for (in ISO-8601 format).
197+
:param end: End of period to get Metrics for (in ISO-8601 format).
198+
:param step: Resolution of results in seconds.
199+
"""
200+
return self._client.get_metrics(
201+
self,
202+
type=type,
203+
start=start,
204+
end=end,
205+
step=step,
206+
)
207+
180208
def get_actions_list(
181209
self,
182210
status: list[str] | None = None,
@@ -533,6 +561,46 @@ def delete(self, load_balancer: LoadBalancer | BoundLoadBalancer) -> bool:
533561
)
534562
return True
535563

564+
def get_metrics(
565+
self,
566+
load_balancer: LoadBalancer | BoundLoadBalancer,
567+
type: MetricsType | list[MetricsType],
568+
start: datetime | str,
569+
end: datetime | str,
570+
step: float | None = None,
571+
) -> GetMetricsResponse:
572+
"""Get Metrics for a LoadBalancer.
573+
574+
:param load_balancer: The Load Balancer to get the metrics for.
575+
:param type: Type of metrics to get.
576+
:param start: Start of period to get Metrics for (in ISO-8601 format).
577+
:param end: End of period to get Metrics for (in ISO-8601 format).
578+
:param step: Resolution of results in seconds.
579+
"""
580+
if not isinstance(type, list):
581+
type = [type]
582+
if isinstance(start, str):
583+
start = isoparse(start)
584+
if isinstance(end, str):
585+
end = isoparse(end)
586+
587+
params: dict[str, Any] = {
588+
"type": ",".join(type),
589+
"start": start.isoformat(),
590+
"end": end.isoformat(),
591+
}
592+
if step is not None:
593+
params["step"] = step
594+
595+
response = self._client.request(
596+
url=f"/load_balancers/{load_balancer.id}/metrics",
597+
method="GET",
598+
params=params,
599+
)
600+
return GetMetricsResponse(
601+
metrics=Metrics(**response["metrics"]),
602+
)
603+
536604
def get_actions_list(
537605
self,
538606
load_balancer: LoadBalancer | BoundLoadBalancer,

hcloud/load_balancers/domain.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any
3+
from typing import TYPE_CHECKING, Any, Literal
44

55
from dateutil.parser import isoparse
66

@@ -11,6 +11,7 @@
1111
from ..certificates import BoundCertificate
1212
from ..load_balancer_types import BoundLoadBalancerType
1313
from ..locations import BoundLocation
14+
from ..metrics import Metrics
1415
from ..networks import BoundNetwork
1516
from ..servers import BoundServer
1617
from .client import BoundLoadBalancer
@@ -508,3 +509,26 @@ def __init__(
508509
):
509510
self.load_balancer = load_balancer
510511
self.action = action
512+
513+
514+
MetricsType = Literal[
515+
"open_connections",
516+
"connections_per_second",
517+
"requests_per_second",
518+
"bandwidth",
519+
]
520+
521+
522+
class GetMetricsResponse(BaseDomain):
523+
"""Get a Load Balancer Metrics Response Domain
524+
525+
:param metrics: The Load Balancer metrics
526+
"""
527+
528+
__slots__ = ("metrics",)
529+
530+
def __init__(
531+
self,
532+
metrics: Metrics,
533+
):
534+
self.metrics = metrics

hcloud/metrics/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from __future__ import annotations
2+
3+
from .domain import Metrics, TimeSeries # noqa: F401

hcloud/metrics/domain.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from typing import Dict, List, Literal, Tuple
5+
6+
from dateutil.parser import isoparse
7+
8+
from ..core import BaseDomain
9+
10+
TimeSeries = Dict[str, Dict[Literal["values"], List[Tuple[float, str]]]]
11+
12+
13+
class Metrics(BaseDomain):
14+
"""Metrics Domain
15+
16+
:param start: Start of period of metrics reported.
17+
:param end: End of period of metrics reported.
18+
:param step: Resolution of results in seconds.
19+
:param time_series: Dict with time series data, using the name of the time series as
20+
key. The metrics timestamps and values are stored in a list of tuples
21+
``[(timestamp, value), ...]``.
22+
"""
23+
24+
start: datetime
25+
end: datetime
26+
step: float
27+
time_series: TimeSeries
28+
29+
__slots__ = (
30+
"start",
31+
"end",
32+
"step",
33+
"time_series",
34+
)
35+
36+
def __init__(
37+
self,
38+
start: str,
39+
end: str,
40+
step: float,
41+
time_series: TimeSeries,
42+
):
43+
self.start = isoparse(start)
44+
self.end = isoparse(end)
45+
self.step = step
46+
self.time_series = time_series

hcloud/servers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .domain import ( # noqa: F401
55
CreateServerResponse,
66
EnableRescueResponse,
7+
GetMetricsResponse,
78
IPv4Address,
89
IPv6Network,
910
PrivateNet,

hcloud/servers/client.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,30 @@
11
from __future__ import annotations
22

33
import warnings
4+
from datetime import datetime
45
from typing import TYPE_CHECKING, Any, NamedTuple
56

7+
from dateutil.parser import isoparse
8+
69
from ..actions import ActionsPageResult, BoundAction, ResourceActionsClient
710
from ..core import BoundModelBase, ClientEntityBase, Meta
811
from ..datacenters import BoundDatacenter
912
from ..firewalls import BoundFirewall
1013
from ..floating_ips import BoundFloatingIP
1114
from ..images import BoundImage, CreateImageResponse
1215
from ..isos import BoundIso
16+
from ..metrics import Metrics
1317
from ..placement_groups import BoundPlacementGroup
1418
from ..primary_ips import BoundPrimaryIP
1519
from ..server_types import BoundServerType
1620
from ..volumes import BoundVolume
1721
from .domain import (
1822
CreateServerResponse,
1923
EnableRescueResponse,
24+
GetMetricsResponse,
2025
IPv4Address,
2126
IPv6Network,
27+
MetricsType,
2228
PrivateNet,
2329
PublicNetwork,
2430
PublicNetworkFirewall,
@@ -210,6 +216,29 @@ def update(
210216
"""
211217
return self._client.update(self, name, labels)
212218

219+
def get_metrics(
220+
self,
221+
type: MetricsType | list[MetricsType],
222+
start: datetime | str,
223+
end: datetime | str,
224+
step: float | None = None,
225+
) -> GetMetricsResponse:
226+
"""Get Metrics for a Server.
227+
228+
:param server: The Server to get the metrics for.
229+
:param type: Type of metrics to get.
230+
:param start: Start of period to get Metrics for (in ISO-8601 format).
231+
:param end: End of period to get Metrics for (in ISO-8601 format).
232+
:param step: Resolution of results in seconds.
233+
"""
234+
return self._client.get_metrics(
235+
self,
236+
type=type,
237+
start=start,
238+
end=end,
239+
step=step,
240+
)
241+
213242
def delete(self) -> BoundAction:
214243
"""Deletes a server. This immediately removes the server from your account, and it is no longer accessible.
215244
@@ -742,6 +771,46 @@ def update(
742771
)
743772
return BoundServer(self, response["server"])
744773

774+
def get_metrics(
775+
self,
776+
server: Server | BoundServer,
777+
type: MetricsType | list[MetricsType],
778+
start: datetime | str,
779+
end: datetime | str,
780+
step: float | None = None,
781+
) -> GetMetricsResponse:
782+
"""Get Metrics for a Server.
783+
784+
:param server: The Server to get the metrics for.
785+
:param type: Type of metrics to get.
786+
:param start: Start of period to get Metrics for (in ISO-8601 format).
787+
:param end: End of period to get Metrics for (in ISO-8601 format).
788+
:param step: Resolution of results in seconds.
789+
"""
790+
if not isinstance(type, list):
791+
type = [type]
792+
if isinstance(start, str):
793+
start = isoparse(start)
794+
if isinstance(end, str):
795+
end = isoparse(end)
796+
797+
params: dict[str, Any] = {
798+
"type": ",".join(type),
799+
"start": start.isoformat(),
800+
"end": end.isoformat(),
801+
}
802+
if step is not None:
803+
params["step"] = step
804+
805+
response = self._client.request(
806+
url=f"/servers/{server.id}/metrics",
807+
method="GET",
808+
params=params,
809+
)
810+
return GetMetricsResponse(
811+
metrics=Metrics(**response["metrics"]),
812+
)
813+
745814
def delete(self, server: Server | BoundServer) -> BoundAction:
746815
"""Deletes a server. This immediately removes the server from your account, and it is no longer accessible.
747816

0 commit comments

Comments
 (0)