To your questions:
The easiest solution would be to implement the logging at the top
Having the exception bubble up to the top level is an absolutely correct and plausible approach. None of the higher layer methods tries to continue some process after the failure, which typically can't succeed. And a well-equipped exception contains all the information necessary for logging. And doing nothing about exceptions helps you keep your code clean and focussed on the main task instead of the failures.
Logging the exception at it's source is obviously the best solution
because I have the most information.
That's half correct. Yes, most useful information is available there. But I'd recommend to put all this into the exception object (if it isn't already there) instead of immediately logging it. If you log at a low level, you still need to throw an exception up to tell your callers that you didn't complete your job. This ends up in multiple logs of the same event.
My main guideline is to catch and log exceptions at the top level only. And all the layers below should make sure that all necessary failure information gets transported up to the top level. Within a single-process application e.g. in Java, this mostly means not to try/catch or log at all outside the top level.
Sometimes, you want some context information included in the exception log that's not available in the original exception, e.g. the SQL statement and parameters that were executed when the exception was thrown. Then you can catch the original exception and re-throw a new one, containing the original one plus the context.
Of course, real life sometimes interferes:
In Java, sometimes you have to catch an exception and wrap it into a different exception type just to obey some fixed method signatures. But if you re-throw an exception, make sure the re-thrown one contains all information needed for later logging.
If you are crossing an inter-process border, often you technically can't transfer the full exception object including the stack trace. And of course the connection might get lost. So here's a point where a service should log exceptions and then try its best to transmit as much of the failure information as possible across the line to its client. The service must make sure the client gets a failure notice, either by receiving a failure response or by running into a timeout in case of a broken connection. This will typically result in the same failure to be logged twice, once inside the service (with more detail) and once in the client's top level.
I'm adding some sentences about logging in general, not only exception-logging.
Besides exceptional situations, you want important activities of your application to get recorded in the log as well. So, use a logging framework.
Be careful about log levels (reading logs where debug information and serious errors are not flagged with different accordingly is a pain!). Typical log levels are:
- ERROR: Some function failed irrecoverably. That doesn't necessarily mean your whole program crashed, but some task couldn't be completed. Typically, you have an exception object describing the failure.
- WARNING: Something strange happened, but didn't cause any task to fail (strange configuration detected, temporary connection breakdown causing some retries, etc.)
- INFO: You want to communicate some significant program action to the local system administrator (starting some service with its configuration and software version, importing data files into the database, users logging into the system, you get the idea...).
- DEBUG: Things you as the developer want to see when you are debugging some issue (but you'll never know in advance what you really need in case of this or that specific bug - if you can foresee it, you'll fix the bug). One thing that's always useful is to log activities on external interfaces.
In production, set the log level to INFO. The results should be useful to a system administrator so he knows what's going on. Expect him to call you for assistance or bug-fixing for every ERROR in the log and half of the WARNINGs.
Enable the DEBUG level only during real debugging sessions.
Group the log entries into appropriate categories (e.g. by the fully-qualified classname of the code that generates the entry), allowing you to switch on debug logs for specific parts of your program.