Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
audit_logger.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
11
12#include <nlohmann/json.hpp>
13
14#include <chrono>
15#include <iomanip>
16#include <sstream>
17
18static auto logger = entropic::log::get("storage.audit_logger");
19
20namespace entropic {
21
28 : config_(config) {}
29
35 std::lock_guard<std::mutex> lock(write_mutex_);
36 if (file_.is_open()) {
37 file_.flush();
38 file_.close();
39 }
40}
41
49 if (!config_.enabled) {
50 logger->info("Audit logging disabled");
51 return true;
52 }
53 std::filesystem::create_directories(config_.log_dir);
54 auto path = config_.log_dir / "audit.jsonl";
55 current_file_size_ = std::filesystem::exists(path)
56 ? std::filesystem::file_size(path) : 0;
57 file_.open(path, std::ios::app);
58 if (!file_.is_open()) {
59 logger->error("Failed to open audit log: {}", path.string());
60 return false;
61 }
62 logger->info("Audit log opened: {}", path.string());
63 return true;
64}
65
72void AuditLogger::record(const AuditEntry& entry) {
73 if (!config_.enabled || !file_.is_open()) {
74 return;
75 }
76 nlohmann::json line = audit_entry_to_json(entry);
77 line["version"] = 1;
78 line["timestamp"] = utc_timestamp();
79 line["session_id"] = config_.session_id;
80 line["sequence"] = static_cast<int64_t>(sequence_.fetch_add(1));
81
82 std::string serialized = line.dump();
83 write_line(serialized);
84 auto seq = entry_count_.fetch_add(1);
85 logger->info("Audit: tool='{}', caller='{}', seq={}",
86 entry.tool_name, entry.caller_id, seq);
87}
88
95void AuditLogger::write_line(const std::string& line) {
96 std::lock_guard<std::mutex> lock(write_mutex_);
97 rotate_if_needed();
98 file_ << line << '\n';
99 current_file_size_ += line.size() + 1;
100 buffered_count_++;
101 bool should_flush = (config_.flush_interval_entries == 0)
102 || (buffered_count_ >= config_.flush_interval_entries);
103 if (should_flush) {
104 file_.flush();
105 buffered_count_ = 0;
106 }
107}
108
115 std::lock_guard<std::mutex> lock(write_mutex_);
116 if (file_.is_open()) {
117 file_.flush();
118 buffered_count_ = 0;
119 }
120}
121
129 return entry_count_.load();
130}
131
138std::filesystem::path AuditLogger::log_path() const {
139 return config_.log_dir / "audit.jsonl";
140}
141
148std::string AuditLogger::utc_timestamp() {
149 auto now = std::chrono::system_clock::now();
150 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
151 now.time_since_epoch()) % 1000;
152 auto time = std::chrono::system_clock::to_time_t(now);
153 std::tm utc{};
154 gmtime_r(&time, &utc);
155 std::ostringstream ss;
156 ss << std::put_time(&utc, "%Y-%m-%dT%H:%M:%S")
157 << '.' << std::setfill('0') << std::setw(3) << ms.count() << 'Z';
158 return ss.str();
159}
160
166void AuditLogger::rotate_if_needed() {
167 if (config_.max_file_size == 0) {
168 return;
169 }
170 if (current_file_size_ < config_.max_file_size) {
171 return;
172 }
173 rotate_files();
174}
175
181void AuditLogger::rotate_files() {
182 file_.close();
183 auto base = config_.log_dir / "audit.jsonl";
184 // Shift existing rotated files (N → N+1), drop oldest
185 for (size_t i = config_.max_files; i >= 1; --i) {
186 auto src = base.string() + "." + std::to_string(i);
187 auto dst = base.string() + "." + std::to_string(i + 1);
188 if (i == config_.max_files) {
189 std::filesystem::remove(src);
190 } else if (std::filesystem::exists(src)) {
191 std::filesystem::rename(src, dst);
192 }
193 }
194 std::filesystem::rename(base, base.string() + ".1");
195 file_.open(base, std::ios::app);
196 current_file_size_ = 0;
197 logger->info("Audit log rotated");
198}
199
211 entropic_hook_point_t /*hook_point*/,
212 const char* context_json,
213 char** modified_json,
214 void* user_data) {
215 *modified_json = nullptr;
216 auto* ctx = static_cast<AuditHookContext*>(user_data);
217 if (ctx == nullptr || ctx->logger == nullptr) {
218 return 0;
219 }
220 try {
221 auto j = nlohmann::json::parse(context_json);
222 AuditEntry entry;
223 entry.tool_name = j.value("tool_name", "");
224 entry.params_json = j.contains("args")
225 ? j["args"].dump() : "{}";
226 entry.result_content = j.value("result", "");
227 entry.elapsed_ms = j.value("elapsed_ms", 0.0);
228 entry.directives_json = j.contains("directives")
229 ? j["directives"].dump() : "[]";
230 populate_from_hook_context(entry, *ctx);
231 ctx->logger->record(entry);
232 } catch (const std::exception& e) {
233 logger->error("Audit hook callback failed: {}", e.what());
234 }
235 return 0;
236}
237
246 const AuditHookContext& ctx) {
247 entry.caller_id = ctx.caller_id ? *ctx.caller_id : "unknown";
249 entry.iteration = ctx.iteration ? *ctx.iteration : 0;
251 ? *ctx.parent_conversation_id : "";
252 entry.result_status = entry.result_content.empty()
253 ? "error" : "success";
254}
255
256} // namespace entropic
AuditHookContext — bridges engine state to audit hook callback.
JSONL audit logger for MCP tool calls.
static int hook_callback(entropic_hook_point_t hook_point, const char *context_json, char **modified_json, void *user_data)
Hook callback for POST_TOOL_CALL integration.
AuditLogger(const AuditLogConfig &config)
Construct with configuration.
bool initialize()
Open the log file and prepare for writing.
void flush()
Force flush buffered entries to disk.
std::filesystem::path log_path() const
Get the file path of the current audit log.
void record(const AuditEntry &entry)
Record a tool call audit entry.
size_t entry_count() const
Get the number of entries recorded this session.
~AuditLogger()
Destructor — flushes and closes the log file.
entropic_hook_point_t
Hook points in the engine lifecycle.
Definition hooks.h:34
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Activate model on GPU (WARM → ACTIVE).
void populate_from_hook_context(AuditEntry &entry, const AuditHookContext &ctx)
Populate AuditEntry fields from AuditHookContext state.
nlohmann::json audit_entry_to_json(const AuditEntry &entry)
Serialize AuditEntry fields to a JSON object.
A single audit log entry for one MCP tool call.
Definition audit_entry.h:30
int delegation_depth
Current delegation depth (0 = lead)
Definition audit_entry.h:38
std::string result_content
Tool result text (full, never truncated)
Definition audit_entry.h:35
double elapsed_ms
Tool execution duration in milliseconds.
Definition audit_entry.h:36
std::string parent_conversation_id
Parent conversation ID (empty for lead)
Definition audit_entry.h:40
int iteration
Engine loop iteration number.
Definition audit_entry.h:39
std::string params_json
Tool parameters as JSON string (never truncated)
Definition audit_entry.h:33
std::string tool_name
Fully-qualified tool name (e.g., "filesystem.write_file")
Definition audit_entry.h:32
std::string result_status
"success", "error", or "timeout"
Definition audit_entry.h:34
std::string caller_id
Identity/tier name (e.g., "eng", "qa", "lead")
Definition audit_entry.h:31
std::string directives_json
Directives array as JSON string ("[]" if none)
Definition audit_entry.h:37
Context passed to AuditLogger hook via user_data pointer.
const int * iteration
Pointer to current iteration.
const int * delegation_depth
Pointer to current depth.
const std::string * caller_id
Pointer to current identity name.
const std::string * parent_conversation_id
Pointer to parent conv ID.
Audit log configuration within StorageConfig.
Definition config.h:483
size_t max_file_size
Rotation size in bytes (0 = unlimited)
Definition config.h:488
size_t flush_interval_entries
Flush every N entries (0 = every entry)
Definition config.h:487
std::string session_id
UUID for this session.
Definition config.h:485
std::filesystem::path log_dir
Directory for audit log files.
Definition config.h:484
bool enabled
Master toggle for audit logging.
Definition config.h:486
size_t max_files
Max rotated files to keep.
Definition config.h:489