Module: LogStruct::Integrations::Shrine

Extended by:
IntegrationInterface, T::Sig
Defined in:
lib/log_struct/integrations/shrine.rb

Overview

Shrine integration for structured logging

Defined Under Namespace

Modules: SkipLoggedEvents

Constant Summary collapse

SHRINE_EVENTS =
T.let(%i[upload exists download delete metadata open].freeze, T::Array[Symbol])
DISPOSITION_FILENAME_REGEX =

The file name in a Content-Disposition (attachment; filename="..."; filename*=UTF-8''...). A quoted name can hold a semicolon, and whatever follows a quote (malformed or not) goes too.

/(\bfilename\*?\s*=\s*)(?:"(?:[^"\\]|\\.)*"?)?[^;]*/i

Class Method Summary collapse

Methods included from IntegrationInterface

setup

Class Method Details

.setup(config) ⇒ Boolean?

Set up Shrine structured logging

Parameters:

Returns:

  • (Boolean, nil)


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/log_struct/integrations/shrine.rb', line 25

def self.setup(config)
  return nil unless defined?(::Shrine)
  return nil unless config.enabled
  return nil unless config.integrations.enable_shrine

  # Create a structured log subscriber for Shrine
  # ActiveSupport::Notifications::Event has name, time, end, transaction_id, payload, and duration
  shrine_log_subscriber = lambda do |event|
    payload = event.payload.except(:io, :name).dup

    # Map event name to Event type
    event_type = case event.name
    when :upload then Event::Upload
    when :download then Event::Download
    when :open then Event::Download
    when :delete then Event::Delete
    when :exists then Event::Exist
    else Event::Metadata
    end

    # Create structured log data
    # Ensure storage is always a symbol
    storage_sym = payload[:storage].to_sym

    log_data = case event_type
    when Event::Upload
       = payload[:metadata].is_a?(Hash) ? payload[:metadata] : {}
      size = ["size"]
      Log::Shrine::Upload.new(
        storage: storage_sym,
        location: payload[:location],
        mime_type: ["mime_type"]&.to_s,
        size: size.is_a?(Integer) ? size : nil,
        uploader: payload[:uploader]&.to_s,
        upload_options: without_file_names(payload[:upload_options]),
        options: payload[:options],
        duration_ms: event.duration.to_f
      )
    when Event::Download
      Log::Shrine::Download.new(
        storage: storage_sym,
        location: payload[:location],
        download_options: payload[:download_options]
      )
    when Event::Delete
      Log::Shrine::Delete.new(
        storage: storage_sym,
        location: payload[:location]
      )
    when Event::Exist
      Log::Shrine::Exist.new(
        storage: storage_sym,
        location: payload[:location]
      )
    else
      # metadata, the only other event in SHRINE_EVENTS
      Log::Shrine::Metadata.new(storage: storage_sym, duration_ms: event.duration.to_f)
    end

    # Log directly through SemanticLogger, NOT through Shrine.logger
    # Shrine.logger is a basic Logger that would just call .to_s on the struct
    ::SemanticLogger[::Shrine].info(log_data)
  end

  # Load the instrumentation plugin without Shrine's default log subscriber, which logs
  # each event as a line of text (a no-op if the app has loaded it already). Shrine
  # ignores a subscriber that's already subscribed.
  ::Shrine.plugin :instrumentation, log_subscriber: nil
  SHRINE_EVENTS.each do |event_name|
    ::Shrine.subscribe(event_name, &shrine_log_subscriber)
  end

  # The app may load the plugin with the default log subscriber too, before or after this
  ::Shrine::Plugins::Instrumentation::LogSubscriber.singleton_class.prepend(SkipLoggedEvents)

  # Other plugins' log subscribers and Shrine's warnings write text to Shrine.logger
  ::Shrine.logger = ::SemanticLogger[::Shrine]

  true
end

.without_file_names(options) ⇒ T.untyped

Upload options with the file names taken out of a Content-Disposition (upload_options: ..., which the upload_options plugin often sets): they can name a person

Parameters:

  • options (T.untyped)

Returns:

  • (T.untyped)


109
110
111
112
113
114
115
116
117
118
119
# File 'lib/log_struct/integrations/shrine.rb', line 109

def self.without_file_names(options)
  return options unless options.is_a?(Hash)

  options.to_h do |key, value|
    if value.is_a?(String) && KeyWords.normalize(key).include?("disposition")
      [key, value.gsub(DISPOSITION_FILENAME_REGEX, '\1[FILENAME]')]
    else
      [key, value]
    end
  end
end