Class: Magick::AuditLog
- Inherits:
-
Object
- Object
- Magick::AuditLog
- Defined in:
- lib/magick/audit_log.rb
Overview
Records one entry per logical feature mutation (see Feature#record_change).
Storage is two-tiered, mirroring Versioning:
* a process-local ring of the last `max_entries` entries across all
features, for fast reads of what this process just did;
* a durable, shared store holding the last `retention` entries per
feature in every adapter that outlives the process (Redis and/or
ActiveRecord), so history survives a restart and every container can
answer "who changed this flag" about changes made anywhere else.
A memory-only deployment has nothing that outlives the process, so it keeps only the ring — that is inherent, not a fallback.
Defined Under Namespace
Constant Summary collapse
- DEFAULT_MAX_ENTRIES =
Entries per process, across all features. Bounds memory in a long-running process; not a durability setting.
10_000- DEFAULT_RETENTION =
Entries kept per feature in the durable, shared store.
200- STORE_PREFIX =
Reserved pseudo-feature namespace the durable store writes under.
'__magick_audit:'
Instance Attribute Summary collapse
-
#max_entries ⇒ Object
readonly
Returns the value of attribute max_entries.
-
#retention ⇒ Object
readonly
Returns the value of attribute retention.
Class Method Summary collapse
-
.merge(entries, limit: nil) ⇒ Object
De-duplicate by id (later wins, so a locally created entry beats its round-tripped copy) and order chronologically.
Instance Method Summary collapse
-
#durable? ⇒ Boolean
True when entries are being written somewhere that survives a restart.
-
#entries(feature_name: nil, limit: 100) ⇒ Object
Merges the durable, shared history with this process's ring, so a process sees both what it did itself and what other processes did.
-
#initialize(adapter = nil, max_entries: DEFAULT_MAX_ENTRIES, retention: DEFAULT_RETENTION, adapter_registry: nil, persist: true) ⇒ AuditLog
constructor
A new instance of AuditLog.
- #log(feature_name, action, user_id: nil, changes: {}, metadata: {}) ⇒ Object
-
#size ⇒ Object
Entries held by THIS process's ring.
Constructor Details
#initialize(adapter = nil, max_entries: DEFAULT_MAX_ENTRIES, retention: DEFAULT_RETENTION, adapter_registry: nil, persist: true) ⇒ AuditLog
Returns a new instance of AuditLog.
238 239 240 241 242 243 244 245 246 247 |
# File 'lib/magick/audit_log.rb', line 238 def initialize(adapter = nil, max_entries: DEFAULT_MAX_ENTRIES, retention: DEFAULT_RETENTION, adapter_registry: nil, persist: true) @adapter = adapter @logs = [] @max_entries = positive_or(max_entries, DEFAULT_MAX_ENTRIES) @retention = positive_or(retention, DEFAULT_RETENTION) @persist = persist != false @adapter_registry = adapter_registry @mutex = Mutex.new end |
Instance Attribute Details
#max_entries ⇒ Object (readonly)
Returns the value of attribute max_entries.
249 250 251 |
# File 'lib/magick/audit_log.rb', line 249 def max_entries @max_entries end |
#retention ⇒ Object (readonly)
Returns the value of attribute retention.
249 250 251 |
# File 'lib/magick/audit_log.rb', line 249 def retention @retention end |
Class Method Details
.merge(entries, limit: nil) ⇒ Object
De-duplicate by id (later wins, so a locally created entry beats its round-tripped copy) and order chronologically.
298 299 300 301 302 303 |
# File 'lib/magick/audit_log.rb', line 298 def self.merge(entries, limit: nil) by_id = {} entries.each { |entry| by_id[entry.id] = entry if entry } merged = by_id.values.sort_by(&:id) limit ? merged.last(limit) : merged end |
Instance Method Details
#durable? ⇒ Boolean
True when entries are being written somewhere that survives a restart.
292 293 294 |
# File 'lib/magick/audit_log.rb', line 292 def durable? store&.durable? || false end |
#entries(feature_name: nil, limit: 100) ⇒ Object
Merges the durable, shared history with this process's ring, so a process sees both what it did itself and what other processes did.
277 278 279 280 281 282 283 |
# File 'lib/magick/audit_log.rb', line 277 def entries(feature_name: nil, limit: 100) local = @mutex.synchronize { @logs.dup } local = local.select { |e| e.feature_name == feature_name.to_s } if feature_name durable = feature_name ? store&.read(feature_name) : store&.read_all self.class.merge(Array(durable) + local, limit: limit) end |
#log(feature_name, action, user_id: nil, changes: {}, metadata: {}) ⇒ Object
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
# File 'lib/magick/audit_log.rb', line 251 def log(feature_name, action, user_id: nil, changes: {}, metadata: {}) entry = Entry.new(feature_name, action, user_id: user_id, changes: changes, metadata: ) recent = @mutex.synchronize do @logs << entry # Cap in-memory ring; older entries fall out once we cross the limit. # This keeps long-running processes from growing @logs unboundedly. @logs.shift while @logs.size > @max_entries @logs.last(@retention) end # Both writes run OUTSIDE @mutex: that lock guards the in-memory ring # only, so a database- or Redis-backed sink never serializes every # feature mutation in the process behind it. persist(entry, recent) append_to_adapter(entry) if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8? Magick::Rails::Events.audit_logged(feature_name, action: action, user_id: user_id, changes: changes, **) end entry end |
#size ⇒ Object
Entries held by THIS process's ring. Durable history is usually larger; read it with #entries.
287 288 289 |
# File 'lib/magick/audit_log.rb', line 287 def size @mutex.synchronize { @logs.size } end |