2 # Copyright 2021 Canonical Ltd.
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
24 from pathlib
import Path
25 from typing
import Dict
, List
, NoReturn
27 from ops
.charm
import CharmBase
28 from ops
.framework
import EventBase
, StoredState
29 from ops
.main
import main
30 from ops
.model
import ActiveStatus
, Application
, BlockedStatus
, MaintenanceStatus
, Unit
31 from oci_image
import OCIImageResource
, OCIImageResourceError
33 from pod_spec
import make_pod_spec
35 logger
= logging
.getLogger(__name__
)
37 PROMETHEUS_KAFKA_EXPORTER_PORT
= 9308
40 class RelationsMissing(Exception):
41 def __init__(self
, missing_relations
: List
):
43 if missing_relations
and isinstance(missing_relations
, list):
44 self
.message
+= f
'Waiting for {", ".join(missing_relations)} relation'
45 if "," in self
.message
:
49 class RelationDefinition
:
50 def __init__(self
, relation_name
: str, keys
: List
, source_type
):
51 if source_type
!= Application
and source_type
!= Unit
:
53 "source_type should be ops.model.Application or ops.model.Unit"
55 self
.relation_name
= relation_name
57 self
.source_type
= source_type
60 def check_missing_relation_data(
62 expected_relations_data
: List
[RelationDefinition
],
64 missing_relations
= []
65 for relation_data
in expected_relations_data
:
67 f
"{relation_data.relation_name}_{k}" in data
for k
in relation_data
.keys
69 missing_relations
.append(relation_data
.relation_name
)
71 raise RelationsMissing(missing_relations
)
74 def get_relation_data(
76 relation_data
: RelationDefinition
,
79 relation
= charm
.model
.get_relation(relation_data
.relation_name
)
82 charm
.app
if relation_data
.source_type
== Application
else charm
.unit
84 expected_type
= relation_data
.source_type
85 for app_unit
in relation
.data
:
86 if app_unit
!= self_app_unit
and isinstance(app_unit
, expected_type
):
87 if all(k
in relation
.data
[app_unit
] for k
in relation_data
.keys
):
88 for k
in relation_data
.keys
:
89 data
[f
"{relation_data.relation_name}_{k}"] = relation
.data
[
96 class PrometheusKafkaExporterCharm(CharmBase
):
97 """Prometheus Kafka Exporter Charm."""
101 def __init__(self
, *args
) -> NoReturn
:
102 """Prometheus Kafka Exporter Charm constructor."""
103 super().__init
__(*args
)
105 # Internal state initialization
106 self
.state
.set_default(pod_spec
=None)
108 self
.port
= PROMETHEUS_KAFKA_EXPORTER_PORT
109 self
.image
= OCIImageResource(self
, "image")
111 # Registering regular events
112 self
.framework
.observe(self
.on
.start
, self
.configure_pod
)
113 self
.framework
.observe(self
.on
.config_changed
, self
.configure_pod
)
115 # Registering required relation events
116 self
.framework
.observe(self
.on
.kafka_relation_changed
, self
.configure_pod
)
118 # Registering required relation departed events
119 self
.framework
.observe(self
.on
.kafka_relation_departed
, self
.configure_pod
)
121 # Registering provided relation events
122 self
.framework
.observe(
123 self
.on
.prometheus_target_relation_joined
, self
._publish
_target
_info
125 self
.framework
.observe(
126 self
.on
.grafana_dashboard_relation_joined
, self
._publish
_dashboard
_info
129 def _publish_target_info(self
, event
: EventBase
) -> NoReturn
:
130 """Publishes target information.
133 event (EventBase): Exporter relation event.
136 "hostname": self
.model
.app
.name
,
137 "port": str(PROMETHEUS_KAFKA_EXPORTER_PORT
),
139 for k
, v
in rel_data
.items():
140 event
.relation
.data
[self
.unit
][k
] = v
142 def _publish_dashboard_info(self
, event
: EventBase
) -> NoReturn
:
143 """Publishes dashboard information.
146 event (EventBase): Exporter relation event.
149 "dashboard": Path("files/kafka_exporter_dashboard.yaml").read_text(),
151 for k
, v
in rel_data
.items():
152 event
.relation
.data
[self
.unit
][k
] = v
155 def relations_requirements(self
):
156 return [RelationDefinition("kafka", ["host", "port"], Unit
)]
158 def get_relation_state(self
):
160 for relation_requirements
in self
.relations_requirements
:
161 data
= get_relation_data(self
, relation_requirements
)
162 relation_state
= {**relation_state
, **data
}
163 check_missing_relation_data(relation_state
, self
.relations_requirements
)
164 return relation_state
166 def configure_pod(self
, _
=None) -> NoReturn
:
167 """Assemble the pod spec and apply it, if possible.
170 event (EventBase): Hook or Relation event that started the
173 if not self
.unit
.is_leader():
174 self
.unit
.status
= ActiveStatus("ready")
177 relation_state
= None
179 relation_state
= self
.get_relation_state()
180 except RelationsMissing
as exc
:
181 logger
.exception("Relation missing error")
182 self
.unit
.status
= BlockedStatus(exc
.message
)
185 self
.unit
.status
= MaintenanceStatus("Assembling pod spec")
187 # Fetch image information
189 self
.unit
.status
= MaintenanceStatus("Fetching image information")
190 image_info
= self
.image
.fetch()
191 except OCIImageResourceError
:
192 self
.unit
.status
= BlockedStatus("Error fetching image information")
196 pod_spec
= make_pod_spec(
203 except ValueError as exc
:
204 logger
.exception("Config/Relation data validation error")
205 self
.unit
.status
= BlockedStatus(str(exc
))
208 if self
.state
.pod_spec
!= pod_spec
:
209 self
.model
.pod
.set_spec(pod_spec
)
210 self
.state
.pod_spec
= pod_spec
212 self
.unit
.status
= ActiveStatus("ready")
215 if __name__
== "__main__":
216 main(PrometheusKafkaExporterCharm
)