Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add configurable logging system #407

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

enigbe
Copy link
Contributor

@enigbe enigbe commented Nov 20, 2024

Overview

This PR introduces a flexible logging system for LDK Node by implementing a LogWriter interface that supports writing logs to different destinations.

What this PR does

  • Introduces a LogWriter interface, allowing Writer variants to handle log output destinations. The supported Writer variants can now:
    • Write logs to the filesystem,
    • Forward to a log implementer,
    • Relay logs to a custom logger.
  • Exposes LogWriter to bindings.
  • Test logging to different destinations with:
    • In-memory log logger,
    • In-memory LogWriter logger.

Related Issue(s)

@enigbe enigbe mentioned this pull request Nov 20, 2024
7 tasks
@G8XSU G8XSU requested a review from tnull November 21, 2024 18:37
Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thank you for looking into this! This already looks pretty good, but I have some comments after the first round of review.

bindings/ldk_node.udl Outdated Show resolved Hide resolved
bindings/ldk_node.udl Outdated Show resolved Hide resolved
@@ -28,13 +28,11 @@ class AndroidLibTest {
config1.storageDirPath = tmpDir1
config1.listeningAddresses = listOf(listenAddress1)
config1.network = Network.REGTEST
config1.logLevel = LogLevel.TRACE
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still set TRACE level, just by the new means?

Copy link
Contributor Author

@enigbe enigbe Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we should. I created a new CustomLogWriter class via which we can set the log level and also test LogWriter. Ran into some issues with NoPointer/Pointer.NULL when testing this (due to unfamiliarity with Kotlin) and would appreciate any pointers to resolve.

Cargo.toml Outdated Show resolved Hide resolved
src/builder.rs Outdated Show resolved Hide resolved
src/logger.rs Outdated
pub(crate) enum Writer {
/// Writes logs to the file system.
FileWriter(FilesystemLogger),
/// Forwards logs to the `log` facade.
LogFacadeWriter(LogFacadeLogger),
/// Forwards logs to custom writer.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: s/custom writer/a custom writer/

}

/// Simple in-memory mock `log` logger for tests.
#[derive(Debug)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this Debug?

Copy link
Contributor Author

@enigbe enigbe Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NodeBuilder is Debug, and it fields an optional LogWriterConfig. The config object has to be Debug and custom log writer variant needs to implement it for this reason.

@@ -271,9 +274,18 @@ impl MockLogger {
}
}

/// [`MockLogger`] as `log` logger - destination for [`Writer::LogFacadeWriter`]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to belong in a different commit? Also, let's not add documents on trait implementations (internal ones in particular).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I thought the documentation added clarity given that the same MockLogger object was being used as a custom logger and a log logger.

I think the MockLogger belongs to the right commit, as I have been careful about what goes into a commit. However, I may have missed this and will be doubly-careful going forward.

@@ -828,13 +831,16 @@ fn generate_bip21_uri() {
},
Err(e) => panic!("Failed to generate URI: {:?}", e),
}

assert!(mock_logger.retrieve_logs().last().unwrap().contains("Invoice created: lnbcrt"));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above: please avoid such potentially-flaky asserts. Also, we should really be testing the log facade separately instead of sprinkling in these asserts to unrelated testcases.

}

#[test]
fn unified_qr_send_receive() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = TestChainSource::Esplora(&electrsd);

// Setup `log` facade logger.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same goes for arbitrary comments: please only add comments where they add to the context. Comments like this don't provide additional information over just reading the next line, they just increase the (visual) noise when reading code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted.

@tnull
Copy link
Collaborator

tnull commented Dec 9, 2024

@enigbe Is there any update on this? Please let me know if you're hitting any blockers.

This seems to need a minor rebase by now.

@enigbe
Copy link
Contributor Author

enigbe commented Dec 10, 2024

No blockers on this. I'll be pushing updates later today.

* Add flexible log writer interface for multiple destinations
* Implement filesystem writing capability via FilesystemLogger
* Prefix LDK-based objects with 'Ldk' for consistency
* Add configuration options for log file path and log level
* Add support for user-provided custom logger to
  write logs to, allowing users to provide any logger
  that implements LogWriter
* Add test to cover this use case, implementing Log-
  Writer for the mock, in-memory MockLogger.
@enigbe enigbe force-pushed the 2024-11-configurable-logging-system branch from 1d57cab to d8eb0e1 Compare December 11, 2024 12:18
This commit includes several improvements aimed at simplifying code, improving readability, and optimizing performance:

- Inline enum struct fields to reduce indirection and clutter.
- Rename structs to improve clarity and better reflect their purpose.
- Enable specific feature flags in dependencies for a more streamlined build.
- Eliminate unnecessary data allocations to optimize memory usage.
- Correct minor grammatical error in documentation.
- allocates Strings only when necessary with `uniffi` feature,
  otherwise, keeps LogRecord fileds as lifetime references.
@enigbe enigbe force-pushed the 2024-11-configurable-logging-system branch from 26aff81 to 7fa2928 Compare December 15, 2024 23:43
This commit adds a CustomLogWriter class to the kotlin library
test, configuring the writer via the exposed node builder and tests
the ability to log to the custom writer destination.
…ehavior

- Removed trait name in description.
- Expanded the description of the `LogWriter` trait to clarify its behavior when the `uniffi` feature is enabled or disabled.
- Improved explanation of the trait’s responsibilities, including handling log messages and forwarding to outputs.
@enigbe enigbe force-pushed the 2024-11-configurable-logging-system branch from 7fa2928 to 283fe85 Compare December 16, 2024 00:32
- Add a custom Debug implementation for LogWriterConfig to work around NodeBuilder's
  Debug constraints.
- Remove the Debug trait from LogWriter, as it is no longer needed due to the custom Debug
   impl on LogWriterConfig.
- Remove the Debug implementation from Writer for consistency.
- Revert MockLogger configuration and assertions
- Remove clutter from setting up LogWriter across all tests
@tnull
Copy link
Collaborator

tnull commented Dec 18, 2024

@enigbe Please let me know if/when this is ready for the next round of review!

@enigbe enigbe force-pushed the 2024-11-configurable-logging-system branch 3 times, most recently from dcf757d to 0660dfa Compare December 19, 2024 21:38
@enigbe enigbe force-pushed the 2024-11-configurable-logging-system branch from 0660dfa to 6766c60 Compare December 19, 2024 21:54
- Remove remnants of TestLogWriter and MockLogger that will be
   reintroduced in a later PR to add a more structured TestConfig
   object.
Copy link
Contributor Author

@enigbe enigbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tnull I believe this is ready for another review. I have addressed the majority of the concerns you raised in the first pass.

Regarding your concerns about testing the logging to custom loggers, I agree that the necessary refactor would be extensive and could detract from the purpose of this PR. As such, I plan to address these test-related changes in a follow-up PR.

Additionally, I encountered some challenges testing a custom logger in Kotlin and would greatly appreciate your guidance or suggestions.

Copy link
Collaborator

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did another round of review, already looks pretty good.

I took a quick look at the Kotlin failures, but also couldn't immediately spot what's up, let me know if you want me to have a closer look though.

Btw, you could consider rebasing on #426 (or on main after it lands) which generally fixes pre-existing CI failures.

@@ -563,6 +569,18 @@ dictionary NodeAnnouncementInfo {
sequence<SocketAddress> addresses;
};

dictionary LogRecord {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since we're (AFAIU?) moving LogLevel up to group with FilesystemLoggerConfig, we should probably do the same with these logging related items.

}
}

pub(crate) fn default_log_file_path() -> String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just use consts for these static default values, no need to add methods on top of it.

self
}

/// Configures the [`Node`] instance to write logs to the `log` facade.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you point the user to the log crate for more information here?

@@ -391,7 +428,8 @@ impl NodeBuilder {
) -> Result<Node, BuildError> {
use bitcoin::key::Secp256k1;

let logger = setup_logger(&self.config)?;
let log_writer_config = self.log_writer_config.clone().unwrap_or_default();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mhh, can we avoid these clones here? Possibly by having setup_logger taking the whole &Option<LogWriterConfig> and implementing the defaulting behavior once there, which probably would be preferable over doing it in several places anyhow?

let log_file_path = if let Some(fp) = &fs_logger_config.log_file_path {
fp
} else {
&default_log_file_path()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, let's just use the consts directly here.

/// Forwards logs to the `log` facade.
LogFacadeWriter { level: LogLevel },
/// Forwards logs to a custom writer.
CustomWriter(Arc<dyn LogWriter + Send + Sync>),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might be able to drop the explicit + Send + Sync here if we already have LogWriter inherit them above?


impl LogWriter for Writer {
fn log(&self, record: LogRecord) {
let log = format!(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, so format! allocates a string on the heap. As we try to avoid allocations (to reduce heap fragmentation where possible), can we a) only create this string when we're sure we need it to log (i.e, after passing the log level filtering), and b) can we avoid it altogether for the CustomWriter case, and possibly even the LogFacadeWriter case? In the latter case we should be able to give the arguments to the respective macros directly, no?

Ok(Self { writer: Writer::FileWriter { file_path: file_path.to_string(), level } })
}

pub fn new_log_facade(level: LogLevel) -> Result<Self, ()> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems new_log_facade and new_custom_writer are infallible, so no reason to have them return a Result.

if let Some(parent_dir) = Path::new(&log_file_path).parent() {
pub(crate) struct Logger {
/// Specifies the logger's writer.
writer: Writer,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Logger only has this one field, why not have it be the enum directly? I.e., rename Writer to Logger and implement what's needed directly on there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a concern about this approach. I have tried it and realized that if we use the Writer (renamed to Logger) directly, we will run into a problem where the compiler will not be able to decide between traits LdkLogger::log(...) and LogWriter::log(...) for any log_*!(...). An easy work-around would be to rename the LogWriter's log to write but we decided against that a while back. How would you suggest we address this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to bother implementing the custom logger on both Android and JVM. Let's pick one, likely JVM as we already do most tests there?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants