Class: Magick::AuditLog

Inherits:
Object
  • Object
show all
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

Classes: Entry, Store

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

Class Method Summary collapse

Instance Method Summary collapse

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.

Returns:

  • (Boolean)


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