Class: Textus::Port::AuditLog
- Inherits:
-
Object
- Object
- Textus::Port::AuditLog
- Defined in:
- lib/textus/port/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
-
#scan(seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil, limit: nil) ⇒ Object
Scan log files with optional filters.
-
#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/port/audit_log.rb', line 17 def initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP) @root = root @path = Textus::Store::Geometry.new(root).audit_log_path @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/port/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/port/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/port/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/port/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 |
#scan(seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil, limit: nil) ⇒ Object
Scan log files with optional filters. Returns parsed row hashes. Lane and timestamp filters are left to the caller (they need manifest resolution and Time parsing the port shouldn’t know about).
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/textus/port/audit_log.rb', line 78 def scan(seq_since: nil, key: nil, role: nil, verb: nil, correlation_id: nil, limit: nil) files = all_log_files return [] if files.empty? rows = [] files.each do |file| File.foreach(file) do |line| parsed = parse_row(line.chomp) next unless parsed && matches?(parsed, seq_since:, key:, role:, verb:, correlation_id:) rows << parsed break if limit && rows.length >= limit end break if limit && rows.length >= limit end rows 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).
100 101 102 103 104 105 106 107 108 |
# File 'lib/textus/port/audit_log.rb', line 100 def verify_integrity return [] unless File.exist?(@path) [].tap do |out| iterate_with_prev_seq do |line, lineno, prev_seq| check_line_integrity(line, lineno, prev_seq, out) end end end |