How (not) to design a Metrics API – Part 2: Delegation & Separation
This is part two in a series of articles on how best to design an application metrics monitoring library, in particular its API, providing versatility both in terms of application of the library across domains & environments and in its implementation by one or more vendors. Please read part 1 if you have not already done so in which we introduce the alternative libraries that will be compared with the JXInsight/OpenCore Metrics Open API.
There are many definitions of what constitutes an “Open API” (or “Openness in an API”) but in our opinion there are at least four key ingredients: delegation, separation, extension and contextualization. In this article the first two will be discussed. The remaining two will be discussed in a forthcoming posting.
Note: When it comes to openness of the implementation we need to look at configuration and integration.
The single most important requirement for any Open API should be the ability to use an alternative implementation that adheres to the API contract whilst offering additional benefits over the default implementation in terms of performance, resource management, reliability, scalability, extensibility, serviceability, tooling, integration and so on. If you can’t switch to an entirely different implementation then it most certainly is not open, irrespective of the ability to access the source, and its debatable whether it really is an API (more so a library).
To use an alternative implementation of the JXInsight/OpenCore Metrics Open API all that is needed is the setting of a service provider interface (SPI) system property specifying the service provider factory class that should be used to create an instance of the actual service provider.
Metrics class, representing the entry point into the API and the only implementation class in the entire API, will on class initialization create an instance of a
MetricsProvider via the class implementation of the
MetricsProviderFactory specified in the above system property.
Below is a snippet code showing what an implementation of the above SPI interfaces would look like.
And here is actual code from the
Metrics class showing call delegation through to the provider.
Metrics class does not expose the underlying service provider factory or service provider.
When its comes to delegation this metrics library, hosted on Github, looks very much like a bunch of implementation classes hobbled together. Whilst the
Metrics class does delegate most of its calls to the exposed
MetricsRegistry class instance there is no means whatsoever to switch in an alternative implementation of the
MetricsRegistry class as a way to bridge (route) calls to a more mature and professionally engineered library. Of course
MetricsRegistry being a class and not an interface makes this all the more impossible but we will come back to this later.
Metrics class also hardwires in the creation of a JMX integration which ideally should be optional, delegated to the underlying implementation and configured externally by operations staff.
This metrics library, also hosted on Github, does offer the means to delegate calls to an alternative implementation of the
MonitorRegistry interface specified by way of a system property though the mechanism is somewhat cumbersome, convoluted and costly.
DefaultMonitorRegistry class which is the entry point into this library creates an instance of itself, with a reference to an alternative implementation of
MonitorRegistry. The static method,
getInstance(), in the
DefaultMonitorRegistry class then returns a reference to the
INSTANCE named field of type
MonitorRegistry (actually itself) which in turn delegates calls to an instance field named
registry of type
This interface coupling is unwarranted and hinders development of utility methods in the entry point class,
DefaultMonitorRegistry, without polluting the
MonitorRegistry interface. Not having a distinct SPI interface also complicates bootstrapping which usually needs additional methods and call backs to be introduced once a library goes beyond toy usage and needs to better manage the lifecycle of the underlying service provider.
To truly achieve delegation in an API there needs to be a separation between the API and one or more possible implementations (library) of the API. Delegation is rarely achievable in practice without the designer ensuring that no signatures or call paths expose an implementation class that prevents (restricts) alternative implementations offering the optimal solution in line with the intended goal(s) of such a library. Excluding supporting
struct (value) and
enum classes all should be interfaces except for the entry point class itself which serves to bootstrap and initialize the runtime.
We believe so strongly in this separation that there is only one class,
Metrics, in the JXInsight/OpenCore Metric Open API and all others are inner interfaces within this class. Except for the SPI interfaces the whole Open API is enclosed in a single class (source) file. There is no implementation exposure or leaking of abstractions for that matter. The
Metrics class ensures that the implementation is never exposed (at least “static”tically speaking).
Below is a class structure view of the JXInsight/OpenCore
Metrics class within our favorite IDE. There are no other implementation (C) classes other than the
Metrics class and no other packages needed by the client. All interface (I) classes are contained within this class.
There is not a single interface used (referenced) in any of the many signatures in this library’s
Timer are all implementation class. It would be impossible to offer any sort of service delegation with this design unless all alternative implementations used the very same namespace and class names unnecessarily (over) exposed (which has implications with regard to intellectual property).
Note: Java interfaces should be used to indicate very clearly what is expected of implementations, communicate to users the possibility of different behavioral characteristics of such across implementations, and afford implementations the opportunity to optimize and tailor to the fullest.
This metrics library also fails the separation test in exposing the
MonitorContext implementation class and its inner
Builder implementation class by way of the
Monitor interface used in the
MonitorRegistry interface. In addition the
MonitorRegistry interface directly references the
AnnotatedObject implementation class which in turn exposes the
AnnotatedAttribute implementation class.
Note: Both open source libraries distribute various utility packages containing implementation classes which only serve to further lock-in client callers and effectively eliminate any possibility of competition in terms of implementation. Dependencies across such packages and the core API package are a mixed bag. It is highly unlikely that any client of either library would be portable to another implementation.
Note: If a library is truly designed to be “open” it should ship with two distributions. One that only includes the actual API itself and the other including both the API and the default (vendor) implementation. JXInsight/OpenCore ships with two libraries that do this very thing – opencore-api.jar and opencore.jar.