Class: Textus::Infra::AuditLog

Inherits:
Object
  • Object
show all
Defined in:
lib/textus/infra/audit_log.rb

Constant Summary collapse

DEFAULT_MAX_SIZE =
10_485_760
DEFAULT_KEEP =
5

Instance Method Summary collapse

Constructor Details

#initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP) ⇒ AuditLog

Returns a new instance of AuditLog.



11
12
13
14
15
16
# File 'lib/textus/infra/audit_log.rb', line 11

def initialize(root, max_size: DEFAULT_MAX_SIZE, keep: DEFAULT_KEEP)
  @root     = root
  @path     = File.join(root, "audit.log")
  @max_size = max_size
  @keep     = keep
end

Instance Method Details

#append(role:, verb:, key:, etag_before:, etag_after:, extras: nil) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/textus/infra/audit_log.rb', line 56

def append(role:, verb:, key:, etag_before:, etag_after:, extras: nil)
  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



18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/textus/infra/audit_log.rb', line 18

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].include?(parsed["verb"])

    last_role = parsed["role"]
  end
  last_role
end

#latest_seqObject



33
34
35
36
37
38
39
40
41
# File 'lib/textus/infra/audit_log.rb', line 33

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.
  meta = read_meta(1)
  return meta["max_seq"] if meta

  0
end

#min_available_seqObject



43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/textus/infra/audit_log.rb', line 43

def min_available_seq
  rotated_metas = (1..@keep).map { |n| read_meta(n) }.compact
  if rotated_metas.any?
    rotated_metas.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_integrityObject

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).



71
72
73
74
75
76
77
78
79
80
# File 'lib/textus/infra/audit_log.rb', line 71

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