Entropic 2.9.4
Local-first agentic inference engine
Loading...
Searching...
No Matches
logging.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
9#include <spdlog/sinks/basic_file_sink.h>
10#include <spdlog/sinks/stdout_color_sinks.h>
11#include <spdlog/sinks/sink.h>
12#include <algorithm>
13#include <array>
14#include <atomic>
15#include <filesystem>
16#include <mutex>
17#include <unordered_map>
18#include <unordered_set>
19
20namespace entropic::log {
21
22static std::once_flag s_init_flag;
23static std::shared_ptr<spdlog::sinks::stderr_color_sink_mt> s_sink;
24
25// gh#59 (v2.3.1): per-handle log dispatcher. Pre-v2.3.1, setup_session()
26// called add_file_sink() which mutated EVERY registered logger globally
27// — so two handles with different log_dirs would each see the other's
28// log lines fan out into their session.log. The dispatcher solves this:
29// a single sink installed once on the default logger consults a
30// thread_local current_handle_id (set via HandleLogScope at API entry)
31// and dispatches to per-handle file sinks. Logs emitted with no
32// handle_id context are silently dropped from the file (they still hit
33// the shared stderr console attached separately).
34
36static thread_local int t_current_handle_id = 0;
37
39class HandleAwareSink final : public spdlog::sinks::sink {
40public:
43 void log(const spdlog::details::log_msg& msg) override {
44 std::shared_ptr<spdlog::sinks::sink> target;
45 int id = t_current_handle_id;
46 {
47 std::lock_guard lk(mu_);
48 auto it = sinks_.find(id);
49 if (it != sinks_.end()) { target = it->second; }
50 }
51 if (target) { target->log(msg); }
52 }
53
55 void flush() override {
56 std::vector<std::shared_ptr<spdlog::sinks::sink>> all;
57 {
58 std::lock_guard lk(mu_);
59 all.reserve(sinks_.size());
60 for (auto& [_, s] : sinks_) { all.push_back(s); }
61 }
62 for (auto& s : all) { s->flush(); }
63 }
64
66 void set_pattern(const std::string& pattern) override {
67 std::lock_guard lk(mu_);
68 pattern_ = pattern;
69 for (auto& [_, s] : sinks_) { s->set_pattern(pattern); }
70 }
71
73 void set_formatter(std::unique_ptr<spdlog::formatter> f) override {
74 // Forward only the pattern shape; the per-handle file sinks
75 // need their own formatter instances since unique_ptr can't be
76 // shared. spdlog uses set_pattern under the hood for typical
77 // use, so this branch is rarely taken.
78 std::lock_guard lk(mu_);
79 if (!sinks_.empty()) {
80 // Apply the same pattern by cloning via set_pattern when
81 // possible; if formatter is a custom one, only the first
82 // sink gets it. Pragmatic — the codebase always uses
83 // set_pattern via set_default_logger's pattern.
84 auto it = sinks_.begin();
85 it->second->set_formatter(std::move(f));
86 }
87 }
88
90 void register_sink(int id, std::shared_ptr<spdlog::sinks::sink> s) {
91 std::lock_guard lk(mu_);
92 sinks_[id] = std::move(s);
93 if (!pattern_.empty()) { sinks_[id]->set_pattern(pattern_); }
94 }
95
97 void unregister_sink(int id) {
98 std::lock_guard lk(mu_);
99 sinks_.erase(id);
100 }
101
104 std::vector<int> registered_ids() const {
105 std::lock_guard lk(mu_);
106 std::vector<int> ids;
107 ids.reserve(sinks_.size());
108 for (auto& [id, _] : sinks_) { ids.push_back(id); }
109 return ids;
110 }
111
112private:
113 mutable std::mutex mu_;
114 std::unordered_map<int, std::shared_ptr<spdlog::sinks::sink>> sinks_;
115 std::string pattern_;
116};
117
118static std::shared_ptr<HandleAwareSink> s_dispatcher;
119
120// gh#59 follow-up (v2.3.7): once the console sink is disabled
121// (set_console_enabled(false)), this flag makes get() strip s_sink
122// from every logger it creates afterward. Without it, loggers built
123// lazily during a turn re-inherit s_sink by copying the default
124// logger's sink list, leaking engine output to fd 2 — which is the
125// TUI's paint stream.
126static std::atomic<bool> s_console_disabled{false};
127
128// gh#58 (v2.2.5): the v2.2.5 sink-dedup workaround. Kept around for
129// backward compatibility with `setup_session()` callers that don't go
130// through `register_handle_log()` yet (e.g. early-init / test fixtures
131// without a handle id). The dispatcher path (v2.3.1) is preferred.
132static std::mutex s_session_paths_mu;
133static std::unordered_set<std::string> s_session_paths;
134
145void init(spdlog::level::level_enum level) {
146 std::call_once(s_init_flag, [level]() {
147 s_sink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();
148 s_sink->set_level(level);
149 // gh#59 (v2.3.1): install the per-handle dispatcher alongside
150 // stderr on the default logger. Subsystems' loggers (created
151 // via get()) inherit both sinks. The dispatcher routes file
152 // writes based on thread_local current handle_id; stderr is
153 // shared across handles (process-global console is acceptable
154 // since operators reading stderr already see everything).
155 s_dispatcher = std::make_shared<HandleAwareSink>();
156 s_dispatcher->set_level(level);
157 auto root = std::make_shared<spdlog::logger>(
158 "entropic",
159 spdlog::sinks_init_list{s_sink, s_dispatcher});
160 spdlog::set_default_logger(root);
161 spdlog::set_level(level);
162 spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%^%l%$] %v");
163 });
164}
165
179void add_file_sink(const std::filesystem::path& path) {
180 std::filesystem::create_directories(path.parent_path());
181 auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
182 path.string(), true); // truncate = true → fresh file each run
183 file_sink->set_level(spdlog::level::trace);
184 file_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
185
186 // Add to default logger (inherited by future get() calls)
187 spdlog::default_logger()->sinks().push_back(file_sink);
188
189 // Add to every already-registered logger
190 spdlog::apply_all([&file_sink](std::shared_ptr<spdlog::logger> l) {
191 l->sinks().push_back(file_sink);
192 });
193
194 // Flush on every message — session logs must survive crashes/early exits
195 spdlog::flush_on(spdlog::level::trace);
196}
197
211std::shared_ptr<spdlog::logger> get(const std::string& name) {
212 auto logger = spdlog::get(name);
213 if (logger) {
214 return logger;
215 }
216 if (!s_sink) {
217 init(spdlog::level::info);
218 }
219 auto default_logger = spdlog::default_logger();
220 auto new_logger = std::make_shared<spdlog::logger>(
221 name, default_logger->sinks().begin(),
222 default_logger->sinks().end());
223 // gh#59 follow-up (v2.3.7): if the console was disabled, the
224 // default logger may still carry s_sink (or a copy raced in);
225 // strip it so this lazily-created logger doesn't leak to stderr.
226 if (s_console_disabled.load() && s_sink) {
227 auto& sinks = new_logger->sinks();
228 sinks.erase(
229 std::remove(sinks.begin(), sinks.end(), s_sink),
230 sinks.end());
231 }
232 new_logger->set_level(default_logger->level());
233 spdlog::register_logger(new_logger);
234 return new_logger;
235}
236
243void set_console_enabled(bool enabled) {
244 if (enabled) {
245 return; // default state; nothing installed/removed here
246 }
247 s_console_disabled.store(true);
248 if (!s_sink) {
249 return;
250 }
251 // Strip s_sink from the default logger so future get() copies a
252 // clean list, and from every already-registered logger.
253 auto strip = [](std::shared_ptr<spdlog::logger> l) {
254 auto& sinks = l->sinks();
255 sinks.erase(
256 std::remove(sinks.begin(), sinks.end(), s_sink),
257 sinks.end());
258 };
259 if (auto dl = spdlog::default_logger()) {
260 strip(dl);
261 }
262 spdlog::apply_all(strip);
263}
264
275void setup_session(const std::filesystem::path& log_dir) {
276 // gh#67 (v2.3.2): historically this function attached a file sink
277 // to the global spdlog logger tree and removed the stderr sink so
278 // a single-handle process routed cleanly to session.log. After
279 // v2.3.1 (gh#59) introduced per-handle routing via the
280 // HandleAwareSink dispatcher, calling `register_handle_log()` from
281 // configure_dir + configure_from_file already attaches the
282 // handle's session.log file as a per-handle sink. If setup_session
283 // ALSO does the legacy global add_file_sink path, every log line
284 // ends up written twice — once via the dispatcher, once via the
285 // duplicated global sink. That's the v2.3.1 doubled-line + SEGV
286 // regression.
287 //
288 // The function stays in the public ABI because external callers
289 // (TUI launchers, test harnesses) may call it directly. It now
290 // does only the side-effect-free part: truncate session_model.log
291 // so a fresh session starts clean. File-sink wiring is owned by
292 // `register_handle_log()` exclusively.
293 if (log_dir.empty()) { return; }
294 auto model_file = log_dir / "session_model.log";
295 std::error_code ec;
296 std::filesystem::create_directories(model_file.parent_path(), ec);
297 FILE* fp = fopen(model_file.string().c_str(), "w");
298 if (fp) { fclose(fp); }
299}
300
301// ── Content escaping ──────────────────────────────────────────
302
316void escape_ansi(const std::string& text, size_t& i,
317 std::string& result)
318{
319 result += "[ESC";
320 if (i + 1 >= text.size() || text[i + 1] != '[') {
321 result += ']';
322 return;
323 }
324 ++i; // skip '['
325 while (i + 1 < text.size() && text[i + 1] != 'm') {
326 ++i;
327 result += text[i];
328 }
329 if (i + 1 < text.size()) { ++i; } // skip 'm'
330 result += ']';
331}
332
339static const std::array<const char*, 256>& escape_table() {
340 static const auto tbl = []() {
341 std::array<const char*, 256> t{};
342 t[static_cast<unsigned char>('\n')] = "\\n";
343 t[static_cast<unsigned char>('\r')] = "\\r";
344 t[static_cast<unsigned char>('\t')] = "\\t";
345 t[static_cast<unsigned char>('{')] = "{{";
346 t[static_cast<unsigned char>('}')] = "}}";
347 return t;
348 }();
349 return tbl;
350}
351
360bool escape_char(char c, std::string& result) {
361 const char* esc = escape_table()[static_cast<unsigned char>(c)];
362 if (!esc) { return false; }
363 result += esc;
364 return true;
365}
366
379std::string escape_content(const std::string& text) {
380 std::string result;
381 result.reserve(text.size() + text.size() / 8);
382 for (size_t i = 0; i < text.size(); ++i) {
383 if (escape_char(text[i], result)) { continue; }
384 if (text[i] == '\033') { escape_ansi(text, i, result); }
385 else { result += text[i]; }
386 }
387 return result;
388}
389
390// ── Formatting helpers ────────────────────────────────────────
391
402 const std::shared_ptr<spdlog::logger>& logger,
403 spdlog::level::level_enum level,
404 const std::string& label,
405 const std::string& text)
406{
407 logger->log(level, "{}: {}", label, escape_content(text));
408}
409
419 const std::shared_ptr<spdlog::logger>& logger,
420 const std::string& label,
421 double ms)
422{
423 logger->info("{}: {:.1f}ms", label, ms);
424}
425
435 const std::shared_ptr<spdlog::logger>& logger,
436 int count,
437 double time_ms)
438{
439 double tok_s = (time_ms > 0.0)
440 ? (static_cast<double>(count) / time_ms * 1000.0) : 0.0;
441 logger->info("{} tokens, {:.1f}ms, {:.1f} tok/s",
442 count, time_ms, tok_s);
443}
444
455 const std::shared_ptr<spdlog::logger>& logger,
456 const std::string& label,
457 const std::string& key,
458 const std::string& value)
459{
460 logger->info("{}: {} -> {}", label, key, value);
461}
462
463// ── gh#59 (v2.3.1): per-handle dispatcher API ─────────────────
464
471 int handle_id, const std::filesystem::path& log_dir)
472{
473 if (handle_id == 0 || log_dir.empty()) { return; }
474 if (!s_dispatcher) { init(spdlog::level::info); }
475
476 auto log_file = log_dir / "session.log";
477 std::filesystem::create_directories(log_file.parent_path());
478 auto file_sink =
479 std::make_shared<spdlog::sinks::basic_file_sink_mt>(
480 log_file.string(), /*truncate=*/true);
481 file_sink->set_level(spdlog::level::trace);
482 file_sink->set_pattern(
483 "[%Y-%m-%d %H:%M:%S.%e] [%n] [%l] %v");
484 s_dispatcher->register_sink(handle_id, file_sink);
485
486 // Match the v2.2.5 setup_session behavior: truncate
487 // session_model.log too so a fresh session starts clean.
488 auto model_file = log_dir / "session_model.log";
489 FILE* fp = fopen(model_file.string().c_str(), "w");
490 if (fp) { fclose(fp); }
491
492 spdlog::flush_on(spdlog::level::trace);
493}
494
500void unregister_handle_log(int handle_id) {
501 if (!s_dispatcher || handle_id == 0) { return; }
502 s_dispatcher->unregister_sink(handle_id);
503}
504
511 return t_current_handle_id;
512}
513
520 : prev_id_(t_current_handle_id) {
521 t_current_handle_id = handle_id;
522}
523
532
533} // namespace entropic::log
Per-handle file sink dispatcher (installed on default logger).
Definition logging.cpp:39
void set_pattern(const std::string &pattern) override
Apply pattern to all registered sinks.
Definition logging.cpp:66
void flush() override
Flush every registered handle sink.
Definition logging.cpp:55
void unregister_sink(int id)
Remove a handle id's sink registration.
Definition logging.cpp:97
void register_sink(int id, std::shared_ptr< spdlog::sinks::sink > s)
Register/replace the sink for a handle id.
Definition logging.cpp:90
std::vector< int > registered_ids() const
Snapshot of registered ids (tests + diagnostics).
Definition logging.cpp:104
void set_formatter(std::unique_ptr< spdlog::formatter > f) override
Spdlog's set_formatter shim.
Definition logging.cpp:73
void log(const spdlog::details::log_msg &msg) override
Route message to the current thread's handle file sink.
Definition logging.cpp:43
~HandleLogScope()
Exit scope; restores previous handle_id.
Definition logging.cpp:529
HandleLogScope(int handle_id)
Enter scope; sets thread current handle_id.
Definition logging.cpp:519
static thread_local int t_current_handle_id
Thread-local current handle id (0 = no scope active).
Definition logging.cpp:36
bool escape_char(char c, std::string &result)
Append escaped form of c if special, else return false.
Definition logging.cpp:360
void escape_ansi(const std::string &text, size_t &i, std::string &result)
Consume an ANSI CSI sequence starting at the ESC byte.
Definition logging.cpp:316
static const std::array< const char *, 256 > & escape_table()
256-entry table mapping bytes to escape strings (nullptr = passthrough).
Definition logging.cpp:339
spdlog initialization and logger access.
ENTROPIC_EXPORT std::string escape_content(const std::string &text)
Escape content for safe spdlog formatting.
Definition logging.cpp:379
ENTROPIC_EXPORT void add_file_sink(const std::filesystem::path &path)
Add a file sink to all loggers (truncated per run).
Definition logging.cpp:179
ENTROPIC_EXPORT int current_handle_id()
gh#59 (v2.3.1): query the current thread's handle_id.
Definition logging.cpp:510
ENTROPIC_EXPORT void log_tokens(const std::shared_ptr< spdlog::logger > &logger, int count, double time_ms)
Log token count with throughput.
Definition logging.cpp:434
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
ENTROPIC_EXPORT void init(spdlog::level::level_enum level=spdlog::level::info)
Initialize the logging subsystem.
Definition logging.cpp:145
ENTROPIC_EXPORT void set_console_enabled(bool enabled)
Enable or disable the stderr console sink process-wide.
Definition logging.cpp:243
ENTROPIC_EXPORT void register_handle_log(int handle_id, const std::filesystem::path &log_dir)
gh#59 (v2.3.1): register a per-handle session.log file.
Definition logging.cpp:470
ENTROPIC_EXPORT void log_content(const std::shared_ptr< spdlog::logger > &logger, spdlog::level::level_enum level, const std::string &label, const std::string &text)
Log full content with escaping applied.
Definition logging.cpp:401
ENTROPIC_EXPORT void setup_session(const std::filesystem::path &log_dir)
Set up session logging for a project directory.
Definition logging.cpp:275
ENTROPIC_EXPORT void log_decision(const std::shared_ptr< spdlog::logger > &logger, const std::string &label, const std::string &key, const std::string &value)
Log a decision/routing event.
Definition logging.cpp:454
ENTROPIC_EXPORT void unregister_handle_log(int handle_id)
gh#59 (v2.3.1): release a handle's session.log file sink.
Definition logging.cpp:500
ENTROPIC_EXPORT void log_timing(const std::shared_ptr< spdlog::logger > &logger, const std::string &label, double ms)
Log a timing measurement.
Definition logging.cpp:418