microfmt
Zero-allocation C++ formatting for constrained environments
Loading...
Searching...
No Matches
extended_sink.hpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Jarosław Pelczar <jarek@jpelczar.com>
2//
3// SPDX-License-Identifier: BSD-2-Clause
4
5#pragma once
6
37#include "../microfmt.hpp"
38#include "extended_msg.hpp"
39#include "sink.hpp"
40#include "sync.hpp"
41#include <atomic>
42#include <concepts>
43#include <cstddef>
44#include <cstdint>
45#include <type_traits>
46
47namespace microfmt::log {
48
56template <typename Tag> struct extended_log_sink_traits;
57
58namespace detail {
59
60template <typename Tag>
62 requires(value_ref<typename extended_log_sink_traits<Tag>::context_type> ctx, const extended_log_msg &msg) {
63 { extended_log_sink_traits<Tag>::full(ctx, msg) } -> std::convertible_to<bool>;
64 };
65
66template <typename Tag>
67concept has_extended_flush = requires(value_ref<typename extended_log_sink_traits<Tag>::context_type> ctx) {
68 extended_log_sink_traits<Tag>::flush(ctx);
69};
70
71} // namespace detail
72
78public:
79 struct vtable {
80 void (*log)(void *ctx, const extended_log_msg &msg) noexcept = nullptr;
82 bool (*full)(void *ctx, const extended_log_msg &msg) noexcept = nullptr;
84 void (*flush)(void *ctx) noexcept = nullptr;
85 };
86
87 constexpr extended_log_sink() noexcept = default;
88
89 template <typename Tag, typename Context, typename Traits = extended_log_sink_traits<Tag>>
90 requires(std::is_convertible_v<Context *, typename Traits::context_type *>)
91 constexpr extended_log_sink(Tag, Context &ctx RELOCO_LIFETIMEBOUND RELOCO_LIFETIME_CAPTURE_BY_THIS,
92 level lvl = level::trace) noexcept
93 : ctx_(&ctx), vtbl_(&s_vtbl<Tag>), lvl_(lvl) {}
94
95 // A temporary context would dangle immediately.
96 template <typename Tag, typename Context>
97 requires(!std::is_lvalue_reference_v<Context>)
98 constexpr extended_log_sink(Tag, Context &&, level = level::trace) = delete;
99
101 void log(const extended_log_msg &msg) noexcept {
102 if (vtbl_ != nullptr && msg.lvl >= lvl_) {
103 vtbl_->log(ctx_.get(), msg);
104 }
105 }
106
108 [[nodiscard]] constexpr bool can_report_full() const noexcept { return vtbl_ != nullptr && vtbl_->full != nullptr; }
109
115 [[nodiscard]] bool full(const extended_log_msg &msg) noexcept {
116 return can_report_full() && msg.lvl >= lvl_ && vtbl_->full(ctx_.get(), msg);
117 }
118
119 [[nodiscard]] constexpr bool can_flush() const noexcept { return vtbl_ != nullptr && vtbl_->flush != nullptr; }
120
121 void flush() noexcept {
122 if (can_flush()) {
123 vtbl_->flush(ctx_.get());
124 }
125 }
126
127 void set_level(level l) noexcept { lvl_ = l; }
128 [[nodiscard]] constexpr bool should_log(level l) const noexcept { return l >= lvl_; }
129 [[nodiscard]] constexpr explicit operator bool() const noexcept { return vtbl_ != nullptr; }
130
131private:
132 template <typename Tag> using ctx_t = typename extended_log_sink_traits<Tag>::context_type;
133
134 template <typename Tag> static void log_entry(void *ctx, const extended_log_msg &msg) noexcept {
135 extended_log_sink_traits<Tag>::log(value_ref<ctx_t<Tag>>(*static_cast<ctx_t<Tag> *>(ctx)), msg);
136 }
137
138 template <typename Tag> [[nodiscard]] static constexpr auto full_entry() noexcept {
139 if constexpr (detail::has_extended_full<Tag>) {
140 return +[](void *ctx, const extended_log_msg &msg) noexcept -> bool {
141 return extended_log_sink_traits<Tag>::full(value_ref<ctx_t<Tag>>(*static_cast<ctx_t<Tag> *>(ctx)), msg);
142 };
143 } else {
144 return static_cast<bool (*)(void *, const extended_log_msg &) noexcept>(nullptr);
145 }
146 }
147
148 template <typename Tag> [[nodiscard]] static constexpr auto flush_entry() noexcept {
149 if constexpr (detail::has_extended_flush<Tag>) {
150 return +[](void *ctx) noexcept {
151 extended_log_sink_traits<Tag>::flush(value_ref<ctx_t<Tag>>(*static_cast<ctx_t<Tag> *>(ctx)));
152 };
153 } else {
154 return static_cast<void (*)(void *) noexcept>(nullptr);
155 }
156 }
157
158 template <typename Tag> static constexpr vtable s_vtbl{&log_entry<Tag>, full_entry<Tag>(), flush_entry<Tag>()};
159
160 value_ptr<void> ctx_{};
161 const vtable *vtbl_{nullptr};
162 level lvl_{level::trace};
163};
164
165template <std::size_t MaxSinks, typename Channel> struct fanout_log_sink_tag {};
166
210template <std::size_t MaxSinks, typename Channel = null_wait_channel> class fanout_log_sink {
211 static_assert(MaxSinks > 0, "fanout_log_sink needs room for at least one sink");
212
213 struct RELOCO_POINTER channel_guard {
214 explicit channel_guard(Channel &c) noexcept : c_(c) { c_.lock(); }
215 ~channel_guard() noexcept { c_.unlock(); }
216 channel_guard(const channel_guard &) = delete;
217 channel_guard &operator=(const channel_guard &) = delete;
218 Channel &c_;
219 };
220
221public:
223
224 fanout_log_sink() noexcept = default;
226 fanout_log_sink &operator=(const fanout_log_sink &) = delete;
227
232 bool add_sink(extended_log_sink s) noexcept {
233 channel_guard g(channel_);
234 const std::size_t n = count_.load(std::memory_order_relaxed);
235 if (n >= MaxSinks) {
236 return false;
237 }
238 sinks_[n] = s;
239 count_.store(n + 1, std::memory_order_release); // publish to lock-free priority writers
240 return true;
241 }
242
244 void set_wait_timeout_us(std::uint64_t us) noexcept { timeout_us_.store(us, std::memory_order_relaxed); }
245
247 [[nodiscard]] extended_log_sink as_sink(level lvl = level::trace) & noexcept RELOCO_LIFETIMEBOUND {
248 return extended_log_sink(tag{}, *this, lvl);
249 }
250
252 void notify_space() noexcept {
253 channel_guard g(channel_);
254 channel_.wake_all();
255 }
256
258 [[nodiscard]] space_notifier notifier() & noexcept RELOCO_LIFETIMEBOUND {
259 return space_notifier(this, [](void *self) noexcept { static_cast<fanout_log_sink *>(self)->notify_space(); });
260 }
261
263 [[nodiscard]] std::size_t dropped() const noexcept { return dropped_.load(std::memory_order_relaxed); }
264
265 void log_impl(const extended_log_msg &msg) noexcept {
266 if (!msg.meta.can_wait()) {
267 write_through(msg);
268 return;
269 }
270
271 bool accepted = false;
272 {
273 channel_guard g(channel_);
274 const std::uint32_t ticket = next_ticket_++;
275 // Strict arrival order: sleep (never spin) until every earlier writer finished.
276 (void)channel_.wait_for(wait_forever, [this, ticket] { return serving_ == ticket; });
277 accepted = !any_full(msg) || channel_.wait_for(timeout_us_.load(std::memory_order_relaxed),
278 [this, &msg] { return !any_full(msg); });
279 }
280
281 if (accepted) {
282 write_through(msg);
283 } else {
284 dropped_.fetch_add(1, std::memory_order_relaxed);
285 }
286
287 channel_guard g(channel_);
288 ++serving_;
289 channel_.wake_all();
290 }
291
292 [[nodiscard]] bool full_impl(const extended_log_msg &msg) noexcept { return any_full(msg); }
293
294 void flush_impl() noexcept {
295 const std::size_t n = count_.load(std::memory_order_acquire);
296 for (std::size_t i = 0; i < n; ++i) {
297 sinks_[i].flush();
298 }
299 }
300
301private:
302 [[nodiscard]] bool any_full(const extended_log_msg &msg) noexcept {
303 const std::size_t n = count_.load(std::memory_order_acquire);
304 for (std::size_t i = 0; i < n; ++i) {
305 if (sinks_[i].full(msg)) {
306 return true;
307 }
308 }
309 return false;
310 }
311
312 void write_through(const extended_log_msg &msg) noexcept {
313 const std::size_t n = count_.load(std::memory_order_acquire);
314 for (std::size_t i = 0; i < n; ++i) {
315 sinks_[i].log(msg);
316 }
317 }
318
319 Channel channel_{};
320 microfmt::array<extended_log_sink, MaxSinks> sinks_{};
321 std::atomic<std::size_t> count_{0};
322 std::uint32_t next_ticket_{0}; // guarded by channel_
323 std::uint32_t serving_{0}; // guarded by channel_
324 std::atomic<std::uint64_t> timeout_us_{0};
325 std::atomic<std::size_t> dropped_{0};
326};
327
328template <std::size_t MaxSinks, typename Channel>
331
332 static void log(value_ref<context_type> ctx, const extended_log_msg &msg) noexcept { ctx->log_impl(msg); }
333 static bool full(value_ref<context_type> ctx, const extended_log_msg &msg) noexcept { return ctx->full_impl(msg); }
334 static void flush(value_ref<context_type> ctx) noexcept { ctx->flush_impl(); }
335};
336
337} // namespace microfmt::log
Type-erased handle to an advanced log sink backend, with a severity threshold. Same two-word-plus-vta...
Definition extended_sink.hpp:77
constexpr bool should_log(level l) const noexcept
Definition extended_sink.hpp:128
bool full(const extended_log_msg &msg) noexcept
Whether msg could not be accepted right now without waiting or overwriting. false for backends that c...
Definition extended_sink.hpp:115
void flush() noexcept
Definition extended_sink.hpp:121
constexpr extended_log_sink(Tag, Context &&, level=level::trace)=delete
constexpr bool can_flush() const noexcept
Definition extended_sink.hpp:119
void log(const extended_log_msg &msg) noexcept
Emits msg unless it is below this sink's threshold.
Definition extended_sink.hpp:101
void set_level(level l) noexcept
Definition extended_sink.hpp:127
constexpr bool can_report_full() const noexcept
Reports whether the backend can say it is full.
Definition extended_sink.hpp:108
constexpr extended_log_sink() noexcept=default
Fixed-capacity sink that forwards each record to up to MaxSinks advanced sinks ("pins"),...
Definition extended_sink.hpp:210
extended_log_sink as_sink(level lvl=level::trace) &noexcept RELOCO_LIFETIMEBOUND
Handle for use as a sink (e.g. by extended_logger).
Definition extended_sink.hpp:247
bool add_sink(extended_log_sink s) noexcept
Appends s; returns false when the table is full. Thread context only. Safe against concurrent writers...
Definition extended_sink.hpp:232
void notify_space() noexcept
Reader side: tells waiting writers that space may have been freed. Thread context.
Definition extended_sink.hpp:252
void log_impl(const extended_log_msg &msg) noexcept
Definition extended_sink.hpp:265
std::size_t dropped() const noexcept
Thread records discarded because a pin stayed full for the whole wait.
Definition extended_sink.hpp:263
space_notifier notifier() &noexcept RELOCO_LIFETIMEBOUND
notify_space as a callback for a pin's consumer.
Definition extended_sink.hpp:258
fanout_log_sink() noexcept=default
void set_wait_timeout_us(std::uint64_t us) noexcept
Longest a turn holder sleeps for space, per record.
Definition extended_sink.hpp:244
void flush_impl() noexcept
Definition extended_sink.hpp:294
bool full_impl(const extended_log_msg &msg) noexcept
Definition extended_sink.hpp:292
Type-erased "space was freed" callback handed to the consumer side of a sink (two words,...
Definition sync.hpp:114
#define MICROFMT_API_CLASS
Definition compat.hpp:181
Definition extended_sink.hpp:67
Definition extended_sink.hpp:61
Log record extended with execution-context metadata (pid, tid, CPU index, context flags) for "advance...
Definition default_log_sink.template.hpp:51
constexpr std::uint64_t wait_forever
timeout_us value meaning "wait without a deadline".
Definition sync.hpp:59
level
Shared log severity levels in ascending order.
Definition log_msg.hpp:22
Type-erased structured logging sinks and console output.
log_msg plus log_meta. Derives from the original record, so it converts to const log_msg & anywhere t...
Definition extended_msg.hpp:62
Definition extended_sink.hpp:79
static bool full(value_ref< context_type > ctx, const extended_log_msg &msg) noexcept
Definition extended_sink.hpp:333
static void flush(value_ref< context_type > ctx) noexcept
Definition extended_sink.hpp:334
static void log(value_ref< context_type > ctx, const extended_log_msg &msg) noexcept
Definition extended_sink.hpp:332
Customization point for extended_log_sink backends.
Definition extended_sink.hpp:56
Definition extended_sink.hpp:165
Locking and wait-channel building blocks shared by the logging stack (basic_logger and the extended s...