Class: Textus::Ports::AuditLog
- Inherits:
-
Object
- Object
- Textus::Ports::AuditLog
- Defined in:
- lib/textus/ports/audit_log.rb
Overview
Append-only audit log adapter: writes and rotates the on-disk audit JSONL under the store root. An instantiable class — it holds collaborators (the root path + size/keep config), so each store binds its own instance. It already satisfied ADR 0109’s single-shape rule (every port is an instantiable class) before that ADR’s Clock/Publisher conversions, so it was unchanged by them.
Constant Summary collapse
- DEFAULT_MAX_SIZE =
10_485_760- DEFAULT_KEEP =
5
Instance Method Summary collapse
- #append(role:, verb:, key:, etag_before:, etag_after:, extras: nil) ⇒ Object
-
#initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP) ⇒ AuditLog
constructor
A new instance of AuditLog.
- #last_writer_for(key) ⇒ Object
- #latest_seq ⇒ Object
- #min_available_seq ⇒ Object
-
#verify_integrity ⇒ Object
Returns an array of integrity-violation descriptors for the on-disk log.
Constructor Details
#initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP) ⇒ AuditLog
Returns a new instance of AuditLog.
17 18 19 20 21 22 |
# File 'lib/textus/ports/audit_log.rb', line 17 def initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP) @root = root @path = Textus::Layout.audit_log(root) @max_size = max_size @keep = keep end |
Instance Method Details
#append(role:, verb:, key:, etag_before:, etag_after:, extras: nil) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 |
# File 'lib/textus/ports/audit_log.rb', line 62 def append(role:, verb:, key:, etag_before:, etag_after:, extras: nil) FileUtils.mkdir_p(File.dirname(@path)) File.open(@path, File::WRONLY | File::APPEND | File::CREAT, 0o644) do |f| f.flock(File::LOCK_EX) next_seq = current_max_seq_unlocked + 1 row = assemble_row(next_seq, { role: role, verb: verb, key: key, etag_before: etag_before, etag_after: etag_after }, extras) f.write(JSON.generate(row) + "\n") f.flush rotate!(f) if f.size > @max_size end end |
#last_writer_for(key) ⇒ Object
24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/textus/ports/audit_log.rb', line 24 def last_writer_for(key) return nil unless File.exist?(@path) last_role = nil File.foreach(@path) do |line| parsed = parse_row(line.chomp) next unless parsed next unless parsed["key"] == key next unless %w[put delete key_delete].include?(parsed["verb"]) last_role = parsed["role"] end last_role end |
#latest_seq ⇒ Object
39 40 41 42 43 44 45 46 47 |
# File 'lib/textus/ports/audit_log.rb', line 39 def latest_seq return scan_max_seq(@path) if File.exist?(@path) && File.size(@path).positive? # Active log is empty/missing — consult the most recent rotated file's sidecar. = (1) return ["max_seq"] if 0 end |
#min_available_seq ⇒ Object
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/textus/ports/audit_log.rb', line 49 def min_available_seq = (1..@keep).map { |n| (n) }.compact if .any? .map { |m| m["min_seq"] }.min elsif File.exist?(@path) File.foreach(@path) do |line| parsed = parse_row(line.chomp) return parsed["seq"] if parsed && parsed["seq"] end nil end end |
#verify_integrity ⇒ Object
Returns an array of integrity-violation descriptors for the on-disk log. Each entry is { “lineno” => Integer, “reason” => String, “detail” => String }. Empty array means the log is well-formed (or doesn’t exist yet).
78 79 80 81 82 83 84 85 86 87 |
# File 'lib/textus/ports/audit_log.rb', line 78 def verify_integrity return [] unless File.exist?(@path) out = [] File.foreach(@path).with_index(1) do |line, lineno| violation = check_line(line.chomp, lineno) out << violation if violation end out end |