quill icon indicating copy to clipboard operation
quill copied to clipboard

Feature request: synchronous mode

Open brianfromoregon opened this issue 10 months ago • 9 comments

Given most of my app logging uses quill it'd be handy to support a synchronous mode for app debugging. When adding std::cout and using breakpoints, neither of those play nicely with async logging. My usual workaround is to duplicate interesting quill log lines with cout log lines so I can see the output at the right times. Fortunately Cursor sees me do it once and the rest is tab tab tab! :-)

I have read the suggestions in #359, and I can sprinkle flush lines around, but it's no better than cout and I still need to recompile. It'd be nice if I could configure quill with a sync mode.

brianfromoregon avatar Feb 04 '25 22:02 brianfromoregon

Hey, I've thought about this many times. I'm not sure if it's worth it. Most of quill's features are designed around async logging, and making a sync version that also achieves very good performance would require a separate implementation—essentially almost a different library. I wouldn't want to introduce a slow sync mode just for the sake of having one, while still calling it a "low-latency logging library" only when async mode is used.

For debugging, you can enable immediate flushing in a debug build by adding:

add_compile_definitions(-DQUILL_IMMEDIATE_FLUSH=1)

This forces a flush after each log statement, making them behave as synchronous without code modifications

https://github.com/odygrd/quill/blob/eed05c0a26486728263e4272c350e3f2d736b081/include/quill/LogMacros.h#L42

odygrd avatar Feb 05 '25 03:02 odygrd

Ok. Sync mode can be fast when paired with memory mapped files, letting the OS take care of slow async flush. Nice for process crash to have recent logs.

On Tue, Feb 4, 2025 at 7:31 PM Odysseas Georgoudis @.***> wrote:

Hey, I've thought about this many times. I'm not sure if it's worth it. Most of Quill's features are designed around async logging, and making a sync version that also achieves very good performance would require a separate implementation—essentially a different library. I wouldn't want to introduce a slow sync mode just for the sake of having one, while still calling it a "low-latency logging library" only when async mode is used.

For debugging, you can enable immediate flushing in a debug build by adding:

add_compile_definitions(-DQUILL_IMMEDIATE_FLUSH=1)

This forces a flush after each log statement, making them behave as synchronous.

https://github.com/odygrd/quill/blob/eed05c0a26486728263e4272c350e3f2d736b081/include/quill/LogMacros.h#L42

— Reply to this email directly, view it on GitHub https://github.com/odygrd/quill/issues/660#issuecomment-2635624401, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJEQU3WIVYNNXNB6I6HCNL2OGAZFAVCNFSM6AAAAABWPWNAW6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMMZVGYZDINBQGE . You are receiving this because you authored the thread.Message ID: @.***>

brianfromoregon avatar Feb 24 '25 21:02 brianfromoregon

I think it really depends on the application's needs and latency requirements and how much and where it is logging. The goal of the library is to allow logging with minimal overhead. With sync logging formatting is very slow, especially for doubles and timestamps, takes considerable time. It would add a lot of additional latency to the hot path. The library only takes a binary copy on the hot path, with everything else handled outside and tries to save even a single nanosecond.

I would like to support a sync mode, but many features in the library are designed to work on a separate thread. Making a sync mode that is both efficient and supports all existing features isn’t straightforward.

Regarding crashes, the library includes a signal handler that ensures logs are flushed even in the event of a crash. Ideally, an application shouldn’t be crashing under normal conditions, so adding sync logging latency to the hot path just for this reason doesn’t seem like a good trade-off. Personally, I find core dumps more useful for debugging crashes, as crashes tend to be easier to diagnose than subtle logic errors.

The only other design that makes sense, in my opinion, is writing binary without formatting to mmap shared memory file (a queue) while using a separate process to decode and format them. However, this approach has its own drawbacks—anonymous memory is usually faster, you can only pass POD types for logging, decoding is slower because you determine the types to decode dynamically. and it requires either launching another process each time or running a daemon. I actually built a prototype for this approach, but the decoding throughput wasn’t as good, and considering the extra complexity, it didn’t seem worth it.

If you're interested, you can check out my prototype here:
https://github.com/odygrd/bitlog

odygrd avatar Feb 24 '25 22:02 odygrd

Thanks for sharing

Your prototype reminds me of https://github.com/morganstanley/binlog

On Mon, Feb 24, 2025 at 2:57 PM Odysseas Georgoudis < @.***> wrote:

I think it really depends on the application's needs and latency requirements. The goal of the library is to allow logging with minimal overhead. With sync logging formatting is very slow, especially for doubles and timestamps, takes considerable time. It would add a lot of additional latency to the hot path. The library only takes a binary copy on the hot path, with everything else handled outside and tries to save even a single nanosecond.

I would like to support a sync mode, but many features in the library are designed to work on a separate thread. Making a sync mode that is both efficient and supports all existing features isn’t straightforward.

Regarding crashes, the library includes a signal handler that ensures logs are flushed even in the event of a crash. Ideally, an application shouldn’t be crashing under normal conditions, so adding sync logging latency to the hot path just for this reason doesn’t seem like a good trade-off. Personally, I find core dumps more useful for debugging crashes, as crashes tend to be easier to diagnose than subtle logic errors.

The only other design that makes sense, in my opinion, is writing binary without formatting to mmap shared memory file (a queue) while using a separate process to decode and format them. However, this approach has its own drawbacks—anonymous memory is usually faster, you can only pass POD types for logging, decoding is slower because you determine the types to decode dynamically. and it requires either launching another process each time or running a daemon. I actually built a prototype for this approach, but the decoding throughput wasn’t as good, and considering the extra complexity, it didn’t seem worth it.

If you're interested, you can check out my prototype here: https://github.com/odygrd/bitlog

— Reply to this email directly, view it on GitHub https://github.com/odygrd/quill/issues/660#issuecomment-2679869593, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJEQUZZY5LISSYKETLMLZD2ROPXBAVCNFSM6AAAAABWPWNAW6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMNZZHA3DSNJZGM . You are receiving this because you authored the thread.Message ID: @.***> [image: odygrd]odygrd left a comment (odygrd/quill#660) https://github.com/odygrd/quill/issues/660#issuecomment-2679869593

I think it really depends on the application's needs and latency requirements. The goal of the library is to allow logging with minimal overhead. With sync logging formatting is very slow, especially for doubles and timestamps, takes considerable time. It would add a lot of additional latency to the hot path. The library only takes a binary copy on the hot path, with everything else handled outside and tries to save even a single nanosecond.

I would like to support a sync mode, but many features in the library are designed to work on a separate thread. Making a sync mode that is both efficient and supports all existing features isn’t straightforward.

Regarding crashes, the library includes a signal handler that ensures logs are flushed even in the event of a crash. Ideally, an application shouldn’t be crashing under normal conditions, so adding sync logging latency to the hot path just for this reason doesn’t seem like a good trade-off. Personally, I find core dumps more useful for debugging crashes, as crashes tend to be easier to diagnose than subtle logic errors.

The only other design that makes sense, in my opinion, is writing binary without formatting to mmap shared memory file (a queue) while using a separate process to decode and format them. However, this approach has its own drawbacks—anonymous memory is usually faster, you can only pass POD types for logging, decoding is slower because you determine the types to decode dynamically. and it requires either launching another process each time or running a daemon. I actually built a prototype for this approach, but the decoding throughput wasn’t as good, and considering the extra complexity, it didn’t seem worth it.

If you're interested, you can check out my prototype here: https://github.com/odygrd/bitlog

— Reply to this email directly, view it on GitHub https://github.com/odygrd/quill/issues/660#issuecomment-2679869593, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJEQUZZY5LISSYKETLMLZD2ROPXBAVCNFSM6AAAAABWPWNAW6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMNZZHA3DSNJZGM . You are receiving this because you authored the thread.Message ID: @.***>

brianfromoregon avatar Feb 25 '25 03:02 brianfromoregon

Similar idea but a bit different, binlog writes binary files that you pass for decoding to bread.

That prototype instead writes to a shared memory ring buffer and a daemon in the background will continuously try to discover any new queues from new processes or decode/format any new messages from the existing into human readable files

odygrd avatar Feb 25 '25 04:02 odygrd

You are welcome to close this issue. Thanks again for your consideration

On Mon, Feb 24, 2025 at 8:42 PM Odysseas Georgoudis < @.***> wrote:

Similar idea but a bit different, binlog writes binary files that you pass for decoding to bread.

That prototype instead writes to a shared memory ring buffer and a daemon in the background will continuously try to discover any new queues from new processes or decode/format any new messages from the existing into human readable files

— Reply to this email directly, view it on GitHub https://github.com/odygrd/quill/issues/660#issuecomment-2680475357, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJEQU6WC7MBEAN5IUWHAYT2RPYEFAVCNFSM6AAAAABWPWNAW6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMOBQGQ3TKMZVG4 . You are receiving this because you authored the thread.Message ID: @.***> [image: odygrd]odygrd left a comment (odygrd/quill#660) https://github.com/odygrd/quill/issues/660#issuecomment-2680475357

Similar idea but a bit different, binlog writes binary files that you pass for decoding to bread.

That prototype instead writes to a shared memory ring buffer and a daemon in the background will continuously try to discover any new queues from new processes or decode/format any new messages from the existing into human readable files

— Reply to this email directly, view it on GitHub https://github.com/odygrd/quill/issues/660#issuecomment-2680475357, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAJEQU6WC7MBEAN5IUWHAYT2RPYEFAVCNFSM6AAAAABWPWNAW6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDMOBQGQ3TKMZVG4 . You are receiving this because you authored the thread.Message ID: @.***>

brianfromoregon avatar Feb 25 '25 04:02 brianfromoregon

came to the issues list to see if there was an open issue for synchronous mode.

I am looking at replacing an in-house built logging library with quill, and one of the features we have is that we can select between synchronous and asynchronous at runtime.

It is known and accepted that async mode is the production-grade fast/performant mode. We would never run an app deployed in prod in sync mode.

However, in backtesting and/or for local debugging, synchronous mode can be very useful.

In backtesting, we replay market data from a file as fast as possible. When using a bounded-dropping front-end queue, this often results in dropped messages, because the backend thread isn't able to drain the queue fast enough.

In prod, when consuming market data asynchronously, this isn't an issue, as the market data updates arrive with sufficient intervals for the backend thread to drain the queues.

It is possible to configure an app at runtime to read from a socket (live market data) or from a file (recorded historical market data). Ideally we don't have to maintain 2 builds of our apps - with the only difference between the 2 that one flushes after every log statement.

steve-lorimer avatar May 26 '25 13:05 steve-lorimer

in master flush option has moved to runtime

https://github.com/odygrd/quill/blob/master/include/quill/core/LoggerBase.h#L154

doing something like this should protect you from dropping messages

if (pcap_replay)
{
   constexpr uint32_t MAX_MESSAGES {100};
   logger->set_immediate_flush(MAX_MESSAGES);
}

odygrd avatar May 27 '25 00:05 odygrd

Thanks! :)

steve-lorimer avatar May 27 '25 08:05 steve-lorimer