Adding Prometheus Mongodb Exporter Charm
[osm/devops.git] / installers / charm / prometheus-mongodb-exporter / src / charm.py
1 #!/usr/bin/env python3
2 # Copyright 2021 Canonical Ltd.
3 #
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
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
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
14 # under the License.
15 #
16 # For those usages not covered by the Apache License, Version 2.0 please
17 # contact: legal@canonical.com
18 #
19 # To get in touch with the maintainers, please contact:
20 # osm-charmers@lists.launchpad.net
21 ##
22
23 import logging
24 from pathlib import Path
25 from typing import Dict, List, NoReturn
26
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
32
33 from pod_spec import make_pod_spec
34
35 logger = logging.getLogger(__name__)
36
37 PROMETHEUS_MONGODB_EXPORTER_PORT = 9216
38
39
40 class RelationsMissing(Exception):
41 def __init__(self, missing_relations: List):
42 self.message = ""
43 if missing_relations and isinstance(missing_relations, list):
44 self.message += f'Waiting for {", ".join(missing_relations)} relation'
45 if "," in self.message:
46 self.message += "s"
47
48
49 class RelationDefinition:
50 def __init__(self, relation_name: str, keys: List, source_type):
51 if source_type != Application and source_type != Unit:
52 raise TypeError(
53 "source_type should be ops.model.Application or ops.model.Unit"
54 )
55 self.relation_name = relation_name
56 self.keys = keys
57 self.source_type = source_type
58
59
60 def check_missing_relation_data(
61 data: Dict,
62 expected_relations_data: List[RelationDefinition],
63 ):
64 missing_relations = []
65 for relation_data in expected_relations_data:
66 if not all(
67 f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
68 ):
69 missing_relations.append(relation_data.relation_name)
70 if missing_relations:
71 raise RelationsMissing(missing_relations)
72
73
74 def get_relation_data(
75 charm: CharmBase,
76 relation_data: RelationDefinition,
77 ) -> Dict:
78 data = {}
79 relation = charm.model.get_relation(relation_data.relation_name)
80 if relation:
81 self_app_unit = (
82 charm.app if relation_data.source_type == Application else charm.unit
83 )
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[
90 app_unit
91 ].get(k)
92 break
93 return data
94
95
96 class PrometheusMongodbExporterCharm(CharmBase):
97 """Prometheus Mongodb Exporter Charm."""
98
99 state = StoredState()
100
101 def __init__(self, *args) -> NoReturn:
102 """Prometheus Mongodb Exporter Charm constructor."""
103 super().__init__(*args)
104
105 # Internal state initialization
106 self.state.set_default(pod_spec=None)
107
108 self.port = PROMETHEUS_MONGODB_EXPORTER_PORT
109 self.image = OCIImageResource(self, "image")
110
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)
114
115 # Registering required relation events
116 self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod)
117
118 # Registering required relation departed events
119 self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod)
120
121 # Registering provided relation events
122 self.framework.observe(
123 self.on.prometheus_target_relation_joined, self._publish_target_info
124 )
125 self.framework.observe(
126 self.on.grafana_dashboard_relation_joined, self._publish_dashboard_info
127 )
128
129 def _publish_target_info(self, event: EventBase) -> NoReturn:
130 """Publishes target information.
131
132 Args:
133 event (EventBase): Exporter relation event.
134 """
135 rel_data = {
136 "hostname": self.model.app.name,
137 "port": str(PROMETHEUS_MONGODB_EXPORTER_PORT),
138 }
139 for k, v in rel_data.items():
140 event.relation.data[self.unit][k] = v
141
142 def _publish_dashboard_info(self, event: EventBase) -> NoReturn:
143 """Publishes dashboard information.
144
145 Args:
146 event (EventBase): Exporter relation event.
147 """
148 rel_data = {
149 "dashboard": Path("files/mongodb_exporter_dashboard.yaml").read_text(),
150 }
151 for k, v in rel_data.items():
152 event.relation.data[self.unit][k] = v
153
154 @property
155 def relations_requirements(self):
156 return [RelationDefinition("mongodb", ["connection_string"], Unit)]
157
158 def get_relation_state(self):
159 relation_state = {}
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
165
166 def configure_pod(self, _=None) -> NoReturn:
167 """Assemble the pod spec and apply it, if possible.
168
169 Args:
170 event (EventBase): Hook or Relation event that started the
171 function.
172 """
173 if not self.unit.is_leader():
174 self.unit.status = ActiveStatus("ready")
175 return
176
177 relation_state = None
178 try:
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)
183 return
184
185 self.unit.status = MaintenanceStatus("Assembling pod spec")
186
187 # Fetch image information
188 try:
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")
193 return
194
195 try:
196 pod_spec = make_pod_spec(
197 image_info,
198 self.model.config,
199 relation_state,
200 self.model.app.name,
201 self.port,
202 )
203 except ValueError as exc:
204 logger.exception("Config/Relation data validation error")
205 self.unit.status = BlockedStatus(str(exc))
206 return
207
208 if self.state.pod_spec != pod_spec:
209 self.model.pod.set_spec(pod_spec)
210 self.state.pod_spec = pod_spec
211
212 self.unit.status = ActiveStatus("ready")
213
214
215 if __name__ == "__main__":
216 main(PrometheusMongodbExporterCharm)