Cross cutting concepts like logging

How do you typically deal with cross cutting concepts like logging, where there is no interface in the standard library, in larger code bases or when using external dependencies? E.g. Python has a standard logging concept where any logging facility can plug into. And it’s clear that is comes with a cost (every logging call is dispatching). But how do you typically solve auch things in Ada?

First there cannot be any standard logging. There are too many cases handled completely differently.

As for measurement data in our middleware (100% Ada) each channel could be marked on/off for logging any time. If you run a mileage test you log only certain channels when certain conditions are met. E.g. say, when a vibration channel leaves the tolerance band one starts logging of some very fast channels to catch up how the drill breaks, when it gets back the logging is halted etc. Of course the cost of dispatching calls is zero in comparison with other overhead, e.g. sampling, pushing data, networking, pre-processing, hard drive I/O etc. If you need to care about dispatching calls you have a serious problem and probably need a different hardware and different ways. E.g. our middleware supports oversampling of the channels when you sample a hundred of measurements and then push them trough the middleware.

Then you must ensure that by a system crash the log will not be lost. Then you must support chunking log files. A complete log could become many gigabytes long. Of course, everything is binary and time stamped. No relational databases, because the overhead of indexing is massive.

I mostly use Ada on Linux systems, so I use an Ada binding to syslog. That pushes most of the work onto syslogd, which is almost infinitely configurable for what it does with log messages. Sadly, systemd, which I am not fond of, is attempting to squeeze out syslog.

So basically you have control over all packages where logging is required and you can apply your custom logger? My question was mainly about heterogenous code bases that also use external packages. E.g. usb_embedded has it’s own logging spec that can implemented using different package bodies.

While I agree that the logging backend needs to be custom or at least configurable, the interface could be “standardized” as you can see in Python, rust (log and tracing crates) and maybe log4j for Java. Also the decision about what should be logged (above which level, for which packages) is a very common requirement for all logging facilities.

So my point is more about technical feasability and not so much about justification.
Would Ada allow some link time binding to the actual logging implementation if we have a standard interface for the log calls? How could this be achieved? Weak binding? Can a library depend on a spec file where the package body is not available at compile time for this library?

What you are looking for is writing a plugin. In Ada it works out of the box. You can do that in Ada and on a very high level, without weak symbols and other linker stuff. This is how our middleware built. Each hardware interface is a plugin. Here it goes:

  • Create an API of tagged types, possibly abstract.
  • Declare a procedure Register for providing a factory. E.g. to create an instance of the logger.

A plugin is deployed as a relocatable library. In the library:

  • Derive from the tagged API your implementation.
  • Upon the library initialization call Register.

That is all. When the library loaded and initialized the dispatching table is automatically adjusted with the new implementation. This gives you your late bindings. The Register call adds a constructing function to the global map name->factory, so that you could create an instance by plugin name. You can also use the dispatching constructor for the purpose, but Register procedure approach is more convenient.

You need to beware some relocatable libraries issues. The library initialization is performed under a lock, therefore you cannot use any tasking during it. A solution is to turn off automatic initialization and do it manually. There is a GPR project library option for that. GNAT Ada provides a procedure you must call to initialize the library. So you load the library using the OS loader and then call this procedure. After that you create an instance and you are done.