1 # Copyright 2023 Canonical Ltd.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
17 This document explains how to use the `JujuTopology` class to
18 create and consume topology information from Juju in a consistent manner.
20 The goal of the Juju topology is to uniquely identify a piece
21 of software running across any of your Juju-managed deployments.
22 This is achieved by combining the following four elements:
30 For a more in-depth description of the concept, as well as a
31 walk-through of it's use-case in observability, see
32 [this blog post](https://juju.is/blog/model-driven-observability-part-2-juju-topology-metrics)
37 This library may be used to create and consume `JujuTopology` objects.
38 The `JujuTopology` class provides three ways to create instances:
40 ### Using the `from_charm` method
42 Enables instantiation by supplying the charm as an argument. When
43 creating topology objects for the current charm, this is the recommended
47 topology = JujuTopology.from_charm(self)
50 ### Using the `from_dict` method
52 Allows for instantion using a dictionary of relation data, like the
53 `scrape_metadata` from Prometheus or the labels of an alert rule. When
54 creating topology objects for remote charms, this is the recommended
58 scrape_metadata = json.loads(relation.data[relation.app].get("scrape_metadata", "{}"))
59 topology = JujuTopology.from_dict(scrape_metadata)
62 ### Using the class constructor
64 Enables instantiation using whatever values you want. While this
65 is useful in some very specific cases, this is almost certainly not
66 what you are looking for as setting these values manually may
67 result in observability metrics which do not uniquely identify a
68 charm in order to provide accurate usage reporting, alerting,
69 horizontal scaling, or other use cases.
72 topology = JujuTopology(
73 model="some-juju-model",
74 model_uuid="00000000-0000-0000-0000-000000000001",
75 application="fancy-juju-application",
76 unit="fancy-juju-application/0",
77 charm_name="fancy-juju-application-k8s",
82 from collections
import OrderedDict
83 from typing
import Dict
, List
, Optional
86 # The unique Charmhub library identifier, never change it
87 LIBID
= "bced1658f20f49d28b88f61f83c2d232"
93 class InvalidUUIDError(Exception):
94 """Invalid UUID was provided."""
96 def __init__(self
, uuid
: str):
97 self
.message
= "'{}' is not a valid UUID.".format(uuid
)
98 super().__init
__(self
.message
)
102 """JujuTopology is used for storing, generating and formatting juju topology information.
104 DEPRECATED: This class is deprecated. Use `pip install cosl` and
105 `from cosl.juju_topology import JujuTopology` instead.
113 unit
: Optional
[str] = None,
114 charm_name
: Optional
[str] = None,
116 """Build a JujuTopology object.
118 A `JujuTopology` object is used for storing and transforming
119 Juju topology information. This information is used to
120 annotate Prometheus scrape jobs and alert rules. Such
121 annotation when applied to scrape jobs helps in identifying
122 the source of the scrapped metrics. On the other hand when
123 applied to alert rules topology information ensures that
124 evaluation of alert expressions is restricted to the source
125 (charm) from which the alert rules were obtained.
128 model: a string name of the Juju model
129 model_uuid: a globally unique string identifier for the Juju model
130 application: an application name as a string
131 unit: a unit name as a string
132 charm_name: name of charm as a string
134 if not self
.is_valid_uuid(model_uuid
):
135 raise InvalidUUIDError(model_uuid
)
138 self
._model
_uuid
= model_uuid
139 self
._application
= application
140 self
._charm
_name
= charm_name
143 def is_valid_uuid(self
, uuid
):
144 """Validate the supplied UUID against the Juju Model UUID pattern.
147 uuid: string that needs to be checked if it is valid v4 UUID.
150 True if parameter is a valid v4 UUID, False otherwise.
153 return str(UUID(uuid
, version
=4)) == uuid
154 except (ValueError, TypeError):
158 def from_charm(cls
, charm
):
159 """Creates a JujuTopology instance by using the model data available on a charm object.
162 charm: a `CharmBase` object for which the `JujuTopology` will be constructed
164 a `JujuTopology` object.
167 model
=charm
.model
.name
,
168 model_uuid
=charm
.model
.uuid
,
169 application
=charm
.model
.app
.name
,
170 unit
=charm
.model
.unit
.name
,
171 charm_name
=charm
.meta
.name
,
175 def from_dict(cls
, data
: dict):
176 """Factory method for creating `JujuTopology` children from a dictionary.
179 data: a dictionary with five keys providing topology information. The keys are
185 `unit` and `charm_name` may be empty, but will result in more limited
186 labels. However, this allows us to support charms without workloads.
189 a `JujuTopology` object.
193 model_uuid
=data
["model_uuid"],
194 application
=data
["application"],
195 unit
=data
.get("unit", ""),
196 charm_name
=data
.get("charm_name", ""),
202 remapped_keys
: Optional
[Dict
[str, str]] = None,
203 excluded_keys
: Optional
[List
[str]] = None,
205 """Format the topology information into an ordered dict.
207 Keeping the dictionary ordered is important to be able to
208 compare dicts without having to resort to deep comparisons.
211 remapped_keys: A dictionary mapping old key names to new key names,
212 which will be substituted when invoked.
213 excluded_keys: A list of key names to exclude from the returned dict.
214 uuid_length: The length to crop the UUID to.
218 ("model", self
.model
),
219 ("model_uuid", self
.model_uuid
),
220 ("application", self
.application
),
222 ("charm_name", self
.charm_name
),
226 ret
= OrderedDict({k
: v
for k
, v
in ret
.items() if k
not in excluded_keys
})
230 (remapped_keys
.get(k
), v
) if remapped_keys
.get(k
) else (k
, v
) for k
, v
in ret
.items() # type: ignore
236 def identifier(self
) -> str:
237 """Format the topology information into a terse string.
239 This crops the model UUID, making it unsuitable for comparisons against
240 anything but other identifiers. Mainly to be used as a display name or file
241 name where long strings might become an issue.
245 model_uuid = "00000000-0000-4000-8000-000000000000", \
246 application = "some-app", \
247 unit = "some-app/1" \
249 'a-model_00000000_some-app'
251 parts
= self
.as_dict(
252 excluded_keys
=["unit", "charm_name"],
255 parts
["model_uuid"] = self
.model_uuid_short
256 values
= parts
.values()
258 return "_".join([str(val
) for val
in values
]).replace("/", "_")
261 def label_matcher_dict(self
) -> Dict
[str, str]:
262 """Format the topology information into a dict with keys having 'juju_' as prefix.
264 Relabelled topology never includes the unit as it would then only match
265 the leader unit (ie. the unit that produced the dict).
267 items
= self
.as_dict(
268 remapped_keys
={"charm_name": "charm"},
269 excluded_keys
=["unit"],
272 return {"juju_{}".format(key
): value
for key
, value
in items
if value
}
275 def label_matchers(self
) -> str:
276 """Format the topology information into a promql/logql label matcher string.
278 Topology label matchers should never include the unit as it
279 would then only match the leader unit (ie. the unit that
280 produced the matchers).
282 items
= self
.label_matcher_dict
.items()
283 return ", ".join(['{}="{}"'.format(key
, value
) for key
, value
in items
if value
])
286 def model(self
) -> str:
287 """Getter for the juju model value."""
291 def model_uuid(self
) -> str:
292 """Getter for the juju model uuid value."""
293 return self
._model
_uuid
296 def model_uuid_short(self
) -> str:
297 """Getter for the juju model value, truncated to the first eight letters."""
298 return self
._model
_uuid
[:8]
301 def application(self
) -> str:
302 """Getter for the juju application value."""
303 return self
._application
306 def charm_name(self
) -> Optional
[str]:
307 """Getter for the juju charm name value."""
308 return self
._charm
_name
311 def unit(self
) -> Optional
[str]:
312 """Getter for the juju unit value."""