Class: Brut::Framework::MCP

Inherits:
Object
  • Object
show all
Defined in:
lib/brut/framework/mcp.rb

Overview

The Master Control Program of Brut. This handles all the bootstrapping and setup of your app. You are not intended to use or interact with this class at all. End of line.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(app_klass:) ⇒ MCP

Create and configure the MCP. The app will not work until #boot! has been called, however most of the core configuration will be available via Brut.container.

In particular, when this initializer is done, the following will be set up:

  • Logging
  • I18n
  • Zeitwerk
  • Initial values and configuration from Config#configure!. Note that some values in there are static and some are lazily-evaluated, i.e. their values will only be calculated when fetched.

In general, you shouldn't have to interact with this class directly, however for posterity, there are basically two ways in which to do so:

  • Create the instance and do not call boot!. This is what you'd do if you can't or don't want to connect to external services like the database. For example, when Brut builds assets, it does not call boot!.
  • Create the instance and immediately call boot!. This is what happens most of the time, in particular when the app is started up by Puma to start serving requests.

What you should avoid doing is creating an instance of this class and performing logic before later calling boot!.

Parameters:

  • app_klass (Class)

    subclass of App representing the Brut app being started up and managed.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/brut/framework/mcp.rb', line 45

def initialize(app_klass:)
  @config    = Brut::Framework::Config.new
  @booted    = false
  @loader    = Zeitwerk::Loader.new
  @config.configure!

  setup_logging
  setup_i18n
  setup_zeitwerk

  @app = app_klass.new
  otel_prefix = if @app.id == @app.organization
                  @app.id
                else
                  "#{@app.organization}.#{@app.id}"
                end
  Brut.container.store(
    "otel_attribute_prefix",
    "String",
    "Prefix for all OTel attributes set by the app",
    otel_prefix
  )
end

Class Method Details

.otel_shutdownObject



21
# File 'lib/brut/framework/mcp.rb', line 21

def self.otel_shutdown = @otel_shutdown

Instance Method Details

#boot!Object

Starts up the internals of Brut and that app so that it can receive requests from the web server. This can make network connections to establish connectivity to external resources.



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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/brut/framework/mcp.rb', line 72

def boot!
  if @booted
    raise "already booted!"
  end
  if Brut.container.debug_zeitwerk?
    @loader.log!
  end
  Kernel.at_exit do
    begin
      Brut.container.sequel_db_handle.disconnect
    rescue Sequel::DatabaseConnectionError
      SemanticLogger[self.class].info "Not connected to database, so not disconnecting"
    end
    otel_configured = OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
    if otel_configured
      if $PROGRAM_NAME =~ /^sidekiq/
        SemanticLogger[self.class].info "Assuming we are sidekiq, which will shutdown OpenTelemetry, so doing nothing", program: $PROGRAM_NAME
      else
        if self.class.otel_shutdown.make_true
          SemanticLogger[self.class].info "Shutting down OpenTelemetry", program: $PROGRAM_NAME
          OpenTelemetry.tracer_provider.shutdown
        else
          SemanticLogger[self.class].info "OpenTelemetry already shutdown", program: $PROGRAM_NAME
        end
      end
    else
      SemanticLogger[self.class].info "OpenTelemetry was not configured, so no shutdown needed", program: $PROGRAM_NAME
    end
  end

  boot_otel!
  boot_postgres!

  if Brut.container.eager_load_classes?
    SemanticLogger["Brut"].info("Eagerly loading app's classes")
    @loader.eager_load
  else
    SemanticLogger["Brut"].info("Lazily loading app's classes")
  end

  @app.boot!

  require "sinatra/base"

  @sinatra_app = Class.new(Sinatra::Base)
  @sinatra_app.include(Brut::SinatraHelpers)

  safely_record_exception = ->(exception,http_status_code) {
    begin
    if exception.nil?
      Brut.container.instrumentation.add_event("error triggered without exception", http_status_code:)
    else
      Brut.container.instrumentation.record_exception(exception, http_status_code:)
    end
    rescue => ex
      begin
        SemanticLogger[self.class].error(
          "Error recording exception",
          original_excerption: exception, 
          exception: ex)
      rescue => ex2
        $stderr.puts "While handling an error recording an exception, we get another error from SemanticLogger." + [
          [ :original_exception,  exception,  ].join(": "),
          [ :exception_from_recording_exception,  ex, ].join(": "),
          [ :exception_from_semantic_logger,  ex2 ].join(": "),

        ].join(", ")
      end
    end
  }

  @app.class.error_blocks.each do |condition,block|
    if condition != :catch_all
      @sinatra_app.error(condition) do
        exception = request.env["sinatra.error"]
        safely_record_exception.(exception, response.status)
        block_args = if exception
                       { exception: }
                     else
                       { http_status_code: response.status }
                     end
        block.(**block_args)
      end
    end
  end
  if @app.class.error_blocks[:catch_all]
    block = @app.class.error_blocks[:catch_all]
    block_args = block.parameters.map { |(type,name)|
      [ name, nil ]
    }.to_h
    @sinatra_app.error(400..999) do
      exception = request.env["sinatra.error"]
      safely_record_exception.(exception, response.status)
      if block_args.key?(:exception)
        block_args[:exception] = exception
      end
      if block_args.key?(:http_status_code)
        block_args[:http_status_code] = response.status
      end
      block.(**block_args)
    end
  else
  end

  message = if Brut.container.project_env.development?
              "Form submission did not include an authenticity token. All forms must include one. To add one, use the `form_tag` helper, or include Brut::FrontEnd::Components::Inputs::CsrfToken somewhere inside your <form> tag"
            else
              "Forbidden"
            end
  default_middlewares = [
    [ Brut::FrontEnd::Middlewares::OpenTelemetrySpan ],
    [ Brut::FrontEnd::Middlewares::AnnotateBrutOwnedPaths ],
    [ Brut::FrontEnd::Middlewares::Favicon ],
    [
      Rack::Protection::AuthenticityToken,
      [
        {
          allow_if: ->(env) { env["brut.owned_path"] },
          message: message,
        }
      ]
    ],
  ]
  if Brut.container.auto_reload_classes?
    default_middlewares << Brut::FrontEnd::Middlewares::ReloadApp
  end

  middlewares = default_middlewares + @app.class.middleware.map { |(middleware,args,block)|
    if !middleware.kind_of?(Class)
      klass = middleware.to_s.split(/::/).reduce(Module) { |mod,part|
        mod.const_get(part)
      }
      [ klass, args, block ]
    else
      [ middleware, args, block ]
    end
  }

  middlewares.each do |(middleware,args,block)|
    @sinatra_app.use(middleware,*args,&block)
  end
  befores = [
    Brut::FrontEnd::RouteHooks::SetupRequestContext,
    Brut::FrontEnd::RouteHooks::LocaleDetection,
  ] + @app.class.before

  afters = [
    Brut::FrontEnd::RouteHooks::AgeFlash,
    Brut.container.csp_class,
    Brut.container.csp_reporting_class,
  ].compact + @app.class.after

  [
    [ befores, :before ],
    [ afters,  :after  ],
  ].each do |hooks,method|
    hooks.each do |klass_name|
      klass = klass_name.to_s.split(/::/).reduce(Module) { |mod,part|
        mod.const_get(part)
      }
      hook_method = klass.instance_method(method)
      @sinatra_app.send(method) do
        args = {}

        Brut.container.instrumentation.span("#{klass_name}.#{method}") do |span|
          hook_method.parameters.each do |(type,name)|
            if name.to_s == "**" || name.to_s == "*"
              raise ArgumentError,"#{method.class}##{method.name} accepts '#{name}' and not keyword args. Define it in your class to accept the keyword arguments your method needs"
            end
            if ![ :key,:keyreq ].include?(type)
              raise ArgumentError,"#{name} is not a keyword arg, but is a #{type}"
            end

            if name == :request_context
              args[name] = Brut::FrontEnd::RequestContext.current
            elsif name == :session
              args[name] = Brut.container.session_class.new(rack_session: session)
            elsif name == :request
              args[name] = request
            elsif name == :response
              args[name] = response
            elsif name == :env
              args[name] = env
            elsif type == :keyreq
              raise ArgumentError,"#{method} argument '#{name}' is required, but it's not available in a #{method} hook"
            else
              # this keyword arg has a default value which will be used
            end
          end

          hook = klass.new
          span.add_prefixed_attributes("#{method}.args",args.map { |k,v| [ k,v.class] }.to_h )
          result = hook.send(method,**args)
          span.add_prefixed_attributes("brut", result_class: result.class)
          case result
          in URI => uri
            redirect to(uri.to_s)
          in Brut::FrontEnd::HttpStatus => http_status
            halt http_status.to_i
          in FalseClass
            halt 500
          in NilClass
            nil
          in TrueClass
            nil
          else
            raise NoMatchingPatternError, "Result from #{method} hook #{klass}'s #{method} method was a #{result.class} (#{result.to_s} as a string), which cannot be used to understand the response to generate. Return nil or true if processing should proceed"
          end
        end
      end
    end
  end
  @app.class.routes.each do |route_block|
    @sinatra_app.instance_eval(&route_block)
  end

  @booted = true
end