When we look at a service call, we typically look at the logging component as simply being part of the core code.
However, we are going to define two layers, the Functional Layer, and the Logging Layer. All of the business logic will be contained in the Functional Layer, while the code that only exists for logging will be contained in the Logging Layer.
We connect the two layers via inheritance. Each class in the Functional Layer that needs to log information will have a corresponding class in the Logging Layer.
A typical class hierarchy in the Functional Layer might look like this:
In comparison, the Logging Layer classes attach themselves to the Functional Layer's hierarchy via inheritance.
All classes in the Logging Layer should ideally follow the Liskov Substitution Principle. What this means in regards to the design of the Logging Layer is that, while there is a clear relationship between the FunctionalBase class and any of the children, there is no direct relationship between the LoggingBase class and the other Logging children classes. This is an intentional design choice. As said before, the Logging classes exists to collect the logging information generated by the Functional classes, and not to provide any new business logic. Thus, there is no reason to design something that requires a LoggingSub3 instance to be passed in, and expect it to also accept a LoggingSub3_2 as well. You would instead design the system to take in a FunctionalSub3, which means it would also accept a FunctionalSub3_2, and by extension it would also accept a LoggingSub3 or LoggingSub3_2 as well.
Next: Code Organization