Timestamp Types¶
Timestamp Recording¶
The library records the timestamp of the log statement on the caller thread at the point the call was made. This approach makes the log statement timestamp more accurate but introduces the possibility of the backend seeing a timestamp in the past, causing an out-of-order timestamp to appear in the log.
Due to the usage of different types of queues, it is possible in rare cases for the timestamp to be recorded but not immediately processed. For example, the caller thread might block (if a blocking queue is used) or take some time to reallocate a new non-blocking queue.
To mitigate this, there is a configurable BackendOptions::log_timestamp_ordering_grace_period
option.
This option delays the reading of timestamps by the backend for a few milliseconds, providing additional time for the frontend to push the messages.
Timestamp Methods¶
There are several ways to get the timestamp on the frontend:
TSC (Time Stamp Counter): This is the fastest method for the frontend, as it simply calls
__rdtsc
and pushes the value of the TSC counter. The backend runs anRdtscClock
which periodically syncs with the wall time. It takes therdtsc
value from the frontend and converts it to wall time. Although therdtsc
clock might drift between resyncs, the difference is generally negligible. However, since the backend thread might be running on a separate core, your processor should support invariant TSC to ensure that the TSC counters between different processors are in sync. The OS usually syncs the counters on startup. When this type of timestamp is used the backend also requires a few seconds for the initial sync of theRdtscClock
, which happens only once on the first usage of the clock.System: This method calls
std::chrono::system_clock::now()
on the frontend and pushes the value to the queue. It is slower but the most accurate method. The backend thread does not require any additional time for initialising the internalRdtscClock
clock as is not used.User: This method allows the user to provide a custom timestamp in nanoseconds since epoch via
UserClockSource
. This is useful, for example, in simulations where you want to display a custom timestamp rather than the current one in the log statements.
Providing a Custom Timestamp¶
1#include "quill/Backend.h"
2#include "quill/Frontend.h"
3#include "quill/LogMacros.h"
4#include "quill/Logger.h"
5#include "quill/UserClockSource.h"
6#include "quill/sinks/ConsoleSink.h"
7
8#include <atomic>
9#include <chrono>
10#include <cstdint>
11#include <utility>
12
13/**
14 * Example demonstrating custom clock usage for logging. This is particularly useful
15 * when simulating events from a specific time period, allowing logs to align with
16 * the simulated time.
17 */
18
19/**
20 * This class needs to be thread-safe, unless only a single thread in the application calling
21 * LOG_ macros from the same logger
22 */
23class SimulatedClock : public quill::UserClockSource
24{
25public:
26 SimulatedClock() = default;
27
28 /**
29 * Required by TimestampClock
30 * @return current time now, in nanoseconds since epoch
31 */
32 uint64_t now() const override { return _timestamp_ns.load(std::memory_order_relaxed); }
33
34 /**
35 * set custom timestamp
36 * @param time_since_epoch timestamp
37 */
38 void set_timestamp(std::chrono::seconds time_since_epoch)
39 {
40 // always convert to nanos
41 _timestamp_ns.store(static_cast<uint64_t>(std::chrono::nanoseconds{time_since_epoch}.count()),
42 std::memory_order_relaxed);
43 }
44
45private:
46 std::atomic<uint64_t> _timestamp_ns{0}; // time since epoch - must always be in nanoseconds
47};
48
49int main()
50{
51 // Start the backend thread
52 quill::BackendOptions backend_options;
53 quill::Backend::start(backend_options);
54
55 // Create a simulated timestamp class. Quill takes a pointer to this class,
56 // and the user is responsible for its lifetime.
57 // Ensure that the instance of this class remains alive for as long as the logger
58 // object exists, until the logger is removed.
59 SimulatedClock simulated_clock;
60
61 // Get the console sink and also create a logger using the simulated clock
62 auto console_sink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
63
64 quill::Logger* logger = quill::Frontend::create_or_get_logger(
65 "root", std::move(console_sink),
66 quill::PatternFormatterOptions{
67 "%(time) %(short_source_location:<28) LOG_%(log_level:<9) %(logger:<12) %(message)",
68 "%D %H:%M:%S.%Qns", quill::Timezone::LocalTime, false},
69 quill::ClockSourceType::User, &simulated_clock);
70
71 // Change the LogLevel to print everything
72 logger->set_log_level(quill::LogLevel::TraceL3);
73
74 // Set our timestamp to Sunday 12 June 2022
75 simulated_clock.set_timestamp(std::chrono::seconds{1655007309});
76 LOG_TRACE_L3(logger, "This is a log trace l3 example {}", 1);
77 LOG_TRACE_L2(logger, "This is a log trace l2 example {} {}", 2, 2.3);
78
79 // update our timestamp
80 simulated_clock.set_timestamp(std::chrono::seconds{1655039000});
81 LOG_INFO(logger, "This is a log info {} example", "string");
82 LOG_DEBUG(logger, "This is a log debug example {}", 4);
83}
Getting a Synchronized Timestamp with the Backend Thread TSC Clock¶
In some cases, when using TSC for log statements, you might want to obtain a timestamp that is synchronized with the timestamp seen in the log statements.
To achieve this, you can use the BackendTscClock
. See the example below:
1#include "quill/Backend.h"
2#include "quill/BackendTscClock.h"
3#include "quill/Frontend.h"
4#include "quill/LogMacros.h"
5#include "quill/Logger.h"
6#include "quill/sinks/ConsoleSink.h"
7
8#include <iostream>
9#include <utility>
10
11int main()
12{
13 quill::Backend::start();
14
15 auto console_sink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sink_id_1");
16
17 // Ensure at least one logger with quill::ClockSourceType::Tsc is created for BackendTscClock to function
18 quill::Logger* logger = quill::Frontend::create_or_get_logger(
19 "root", std::move(console_sink),
20 quill::PatternFormatterOptions{
21 "%(time) [%(thread_id)] %(short_source_location:<28) LOG_%(log_level:<9) %(logger:<12) "
22 "%(message)",
23 "%H:%M:%S.%Qns", quill::Timezone::LocalTime},
24 quill::ClockSourceType::Tsc);
25
26 // Log an informational message which will also init the backend RdtscClock
27 LOG_INFO(logger, "This is a log info example with number {}", 123);
28
29 // The function `quill::detail::BackendManager::instance().convert_rdtsc_to_epoch_time(quill::detail::rdtsc())`
30 // will return a valid timestamp only after the backend worker has started and processed
31 // at least one log with `ClockSourceType::Tsc`.
32 // This is because the Rdtsc clock is lazily initialized by the backend worker on the first log message.
33 // To ensure at least one log message is processed, we call flush_log here.
34 logger->flush_log();
35
36 // Get a timestamp synchronized with the backend's clock
37 uint64_t const backend_timestamp = quill::BackendTscClock::now().time_since_epoch().count();
38 std::cout << "Synchronized timestamp with the backend: " << backend_timestamp << std::endl;
39
40 return 0;
41}