[DO NOT MERGE] POC middlewares
This POC is meant to explore the possibility of adding a functionality to ops called Middlewares. The idea is to offer to charmers an easier way to decorate charms and effectively inject pre-charm-init, and post-charm-init hooks, without touching the charm code itself.
Charm libs such as charm_tracing and charm_logging at the moment expose class decorators that have to do some rather hacky stuff in order to override the charm's __init__ to do very important things. Middlewares would allow us to skip some of those shenanigans and hook into the charm initialization sequence with fewer risks.
In the future we're planning to add more decorators, what is going to happen when we have four, five decorators each one of them monkey-patching the charm's init method in different ways? Can't wait to find out.
tandems:
- charm code: https://github.com/canonical/tempo-k8s-operator/pull/146
- testing: https://github.com/canonical/ops-scenario/pull/146
Hey @PietroPasotti, not sure whether you want review on this yet or not?
My first thought at a quick glance is why this can't just be done with simple function calls at the top or bottom of the charm's __init__. In addition, the existing class decorators seem simple and reasonable to me, for example. Where's the hacky stuff and monkey patching?
A high-level comment before I dig into the referenced logging and tracing decorators:
What’s better to decorate/middlewear-ise, ops.main where the effect conceptually applies to every instantiated CharmBase subclass (incl. the little guys in libs) or the specific, typically main CharmBase subclass?
Hey @PietroPasotti, not sure whether you want review on this yet or not?
My first thought at a quick glance is why this can't just be done with simple function calls at the top or bottom of the charm's
__init__. In addition, the existing class decorators seem simple and reasonable to me, for example. Where's the hacky stuff and monkey patching?
here: https://github.com/canonical/tempo-k8s-operator/blob/31a747460a7f71d270dd786e17660d7e4b7fd3f8/lib/charms/tempo_k8s/v1/charm_tracing.py#L362C5-L362C33
the main reason for not wanting to put this in init is that I want it to execute before the charm's init runs, so we can gather metrics from the init itself (and the objects that are initialized in init). For example, charm tracing needs access to the TracingEndpointRequirer object, but we would also like to instrument that object. Hence the need to forward-declare things: we need values from things we want to observe.
A high-level comment before I dig into the referenced logging and tracing decorators:
What’s better to decorate/middlewear-ise,
ops.mainwhere the effect conceptually applies to every instantiated CharmBase subclass (incl. the little guys in libs) or the specific, typically main CharmBase subclass?
the little guys in libs are typically not CharmBase subclasses but Object subclasses, so I don't see much risk of confusion there. Indeed the intention is that you only middleware-ise the single charm type that you're calling ops.main on, not all Objects or other CharmBase instances you might be creating along the way.
Personally I don't see how ops.main(MyCharm, middlewares=...) might give the impression you're middleware-ising anything other than MyCharm, but I may be wrong.
The Overall Need
I tend to think that logging and tracing are very unique cases.
Logs are already shipped to juju in two ways: juju captures container standard error; and ops installs a logging handler that pushes logs via hook tools. In an ideal world, handling the logs becomes juju controller's responsibility from that point on.
Tracing perhaps deserves a first-party hook in the ops framework, the implementation remaining a bring-your-own affair similar to what charm_tracing provides. Juju is likely to gain some form of tracing/telemetry eventually, hopefully the current integration setup will become much easier then.
We'd be very happy to understand the use case that prompted this PR and/or possibly tracing hooks, please come to talk to us during our office hours, e.g. next Tuesday 🙇🏻
The Mechanism
so we can gather metrics from the init itself (and the objects that are initialized in init).
Let's consider this idiom:
class AuguryCharm(CharmBase):
def __init__(...):
tracing.start()
super().__init__(...)
framework.observe(...)
This flow could be implemented explicitly, or using a decorator, an intermediate base class, a mix-in, a meta-class or even a wrapper around ops.main(). It's not clear that middleware infrastructure is required here.
I would gladly hear your thoughts at office hours!