Extensible Abstract Types, Backend Services and Subsystems

In the past couple of years I have probably written between 45 and 60 plugins integrating into various platforms and systems. Certain building blocks repeat a lot... Logging, metrics, templating, state- and secret management.

The catch is that the implementation is not something you always want to tie down to a specific backend: for instance, you can develop a LoggerConfigNode that logs to the console. But what if you want it to log to a file instead? You can solve this by adding a little selection box and to log either to Console or to File. But what about logging to a REST-Endpoint? Easy enough: add a another item to the Log Type dropdown, extend the UI to cater for REST-Endpoint configuration, and implement the backend. Want to log to Graylog? Same story. Loki? Same story... And before you realize, you now have 20 different logging platforms, and a massive UI, 95% of which is hidden depending on which logging platform you selected and a 150Mb node_modules folder.

There is nothing wrong with this pattern, but it means that if someone wants to create their own logger, they now have to clone your plugin, add their logger and try to convince you to accept their pull request.

The alternative would be to have an intermediary and invert the dependency. Instead of having a single logger plugin, have as many as you want as long as they extend the "Abstract Logging Node". From the Nodes perspective, they just have to log to the "Abstract Logging Node". The node doesn't worry whether its logging to A file or to Loki. its the same thing.

This allows you to build your own logger and only install the loggers you want. Your nodes can log to any logger, and any logger should be able to accept log messages from you.

There are however several Caveats in this:
The first thing is that NodeRed does not understand the concept of hierarchies when it comes to Config Nodes. You can only declare one type and that's it. Something needs to allow nodes to query the hierarchy. The way I ended up solving this was by first building a "Service" that is spun up in the backend. The type Service is really lightweight (a couple lines of typescript and no external dependencies), and is included in every plugin that uses it. That way it doesn't require you to install any additional plugins.

Once the nodes are registered, the service just sits there in the background, waiting for a query request from the frontend. The service responds with a list of nodes that can be instantiated.

The only catch here is that there is no sensible way to display this. The solution was to clone the way the config node is rendered in the editor, and hook some other endpoints to it: For the instance selection, I would scan the current Flow on the front-end and find any node that has a type in our list of "Known Logger Types" and add them to the instance selection.

For the button, I would just show a dropdown list of the types and allow the user to select the one they want, and then show the Editor UI for that specific node type.

The next issue was the sequence in which nodes are started during deployment: NodeRed will look at the types of properties and if they are a NodeType and have a value that matches the ID of a node, it will see that as a dependency. The problem is that we wont actually ever know what type of node it is... All we know is the id of said node. To resolve this, we created a "DelegatedConifgRefrenceNode" that does nothing except be a node with an id.When nodered tries to start up our node, it looks at the configuration and see that there is a node with a specific Id (the type is "DelegatedConfigRefrenceNode" but the Id belongs to a type of Log Config Node), and then moves THIS node to the back of the queue to instantiate the DelegatedConfigRefernceNode (or more accurately, the XXXXX Log Config Node) first.The additional benefit was that this also exploited the way that Nodered does reference counting for ConfigNodes. The Flow node might be referencing the "Do-Nothing" DelegatedConfigReferenceNode, but the ID references the concrete implementation of a Log Config Node, consequently it ends up getting counted.

You can implement this pattern for pretty much any type of "Abstract Concept". So far, I have done a couple of these implementations

Type Description Implementations
Logging A logging interface that allows a node to write debug information to a variety of sources NullLogger, Console Logger, Rest Logger, Loki Logger, File Logger
Metrics A metrics interface where nodes can publish various performance statistics (Counters, Timers and Gauges) NullMetricsProvider, Prometheus and StatsD
State A mechanism for persisting a state variable that can be read or set. InternalStateProvider, Zookeeper
Secrets Provides a mechansim to mark properties as secrets and then externalize their provisioning and storage InternalSecretProvider, OpenBaoSecretProvider