brut 0.0.10 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e624fb125bcb14a88b3b7adb148561c404befc86974a77423b6cd66293bba31
4
- data.tar.gz: e2a20fc23b6aa7d0708ea7adf9852333b10616aa4e633d972031c726b9d43316
3
+ metadata.gz: 75470f9c0becdf6066db6bf7d4a53f170e783e2e9a664cc5eef779d977427c3a
4
+ data.tar.gz: 70006b0aba279306c4f7f99bde119b2d348a55b383179b70955240e5bc39be01
5
5
  SHA512:
6
- metadata.gz: 59b7398fbf77fafd04c770e617b8fcb341887db4b14691828ab94775c4d36b44d8475889293a39a8cfc92c7b27ef935c487c60d8f7a7f25f96edbfa57db8b75b
7
- data.tar.gz: aa3ce63a03296837fb5c425ce430a8a0658d732edf32fbed4ac7d01d74fb39dfde6e0b91e0aeadfb7f26d970ac3f23fcf6aab3216354152884fe9f86f4772412
6
+ metadata.gz: 6cfdd32b6fe7595d3a8215d5ed5a9bc8ec33f6093284fbf82be365c2bd7ca2a1e92695ab63a0cb629424ff0877f726647b1853433ab620f694a8de9b130fd852
7
+ data.tar.gz: 2487a01fa0138416af5821c4699c68febd1bffb16be906e53cc2b39c0315f6cf0fc26c87d20b23de975eaef52b683ae495a8ab09b21b944f074e78db7adeb5cb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brut (0.0.10)
4
+ brut (0.0.11)
5
5
  concurrent-ruby
6
6
  i18n
7
7
  irb
data/lib/brut/cli/app.rb CHANGED
@@ -211,7 +211,7 @@ class Brut::CLI::App
211
211
  if self.class.configure_only?
212
212
  as_execution_result(command.execute)
213
213
  else
214
- result = Brut.container.instrumentation.span("CLI.#{$0}", class: self.class.name) do |span|
214
+ result = Brut.container.instrumentation.span("CLI #{$0}", prefix: "brut.cli", class: self.class.name) do |span|
215
215
  as_execution_result(command.execute)
216
216
  end
217
217
  result
@@ -47,11 +47,11 @@ class Brut::CLI::Apps::Test < Brut::CLI::App
47
47
  end
48
48
  Brut.container.instrumentation.span("tests.run") do |span|
49
49
  if args.empty?
50
- span.add_attributes(tests: :all)
50
+ span.add_prefixed_attributes("brut.cli.test", tests: :all)
51
51
  out.puts "Running all tests"
52
52
  system! "#{rspec_command} #{Brut.container.app_specs_dir}/"
53
53
  else
54
- span.add_attributes(tests: args.length)
54
+ span.add_prefixed_attributes("brut.cli.test", tests: args.length)
55
55
  test_args = args.map { |_|
56
56
  '"' + Shellwords.escape(_) + '"'
57
57
  }.join(" ")
@@ -53,6 +53,17 @@ class Brut::Framework::MCP
53
53
  setup_zeitwerk
54
54
 
55
55
  @app = app_klass.new
56
+ otel_prefix = if @app.id == @app.organization
57
+ @app.id
58
+ else
59
+ "#{@app.organization}.#{@app.id}"
60
+ end
61
+ Brut.container.store(
62
+ "otel_attribute_prefix",
63
+ "String",
64
+ "Prefix for all OTel attributes set by the app",
65
+ otel_prefix
66
+ )
56
67
  end
57
68
 
58
69
  # Starts up the internals of Brut and that app so that it can receive requests from
@@ -88,8 +99,8 @@ class Brut::Framework::MCP
88
99
  end
89
100
  end
90
101
 
91
- boot_postgres!
92
102
  boot_otel!
103
+ boot_postgres!
93
104
 
94
105
  if Brut.container.eager_load_classes?
95
106
  SemanticLogger["Brut"].info("Eagerly loading app's classes")
@@ -194,7 +205,7 @@ class Brut::Framework::MCP
194
205
  hook = klass.new
195
206
  span.add_prefixed_attributes("#{method}.args",args.map { |k,v| [ k,v.class] }.to_h )
196
207
  result = hook.send(method,**args)
197
- span.add_attributes(result:)
208
+ span.add_prefixed_attributes("brut", result_class: result.class)
198
209
  case result
199
210
  in URI => uri
200
211
  redirect to(uri.to_s)
@@ -289,6 +300,7 @@ private
289
300
  Sequel::Database.extension :pg_json
290
301
 
291
302
  sequel_db = Brut.container.sequel_db_handle
303
+ sequel_db.extension :brut_instrumentation
292
304
 
293
305
  Sequel::Model.db = sequel_db
294
306
 
@@ -300,7 +312,6 @@ private
300
312
  if !Brut.container.external_id_prefix.nil?
301
313
  Sequel::Model.plugin :external_id, global_prefix: Brut.container.external_id_prefix
302
314
  end
303
- Sequel::Database.extension :brut_instrumentation
304
315
  end
305
316
 
306
317
  def boot_otel!
@@ -79,7 +79,8 @@ class Brut::FrontEnd::Component
79
79
  #
80
80
  # @return [Brut::FrontEnd::Templates::HTMLSafeString] string containing the component's HTML.
81
81
  def render
82
- Brut.container.instrumentation.span("#{self.class}.render") do |span|
82
+ Brut.container.instrumentation.span("#{self.class} render") do |span|
83
+ span.add_prefixed_attributes("brut", type: :component, class: self.class.name)
83
84
  Brut.container.component_locator.locate(self.template_name).
84
85
  then { Brut::FrontEnd::Template.new(it) }.
85
86
  then { it.render_template(self).html_safe! }
@@ -133,7 +134,7 @@ class Brut::FrontEnd::Component
133
134
  # @return [Brut::FrontEnd::Templates::HTMLSafeString] of the rendered component.
134
135
  def component(component_instance,&block)
135
136
  component_name = component_instance.kind_of?(Class) ? component_instance.name : component_instance.class.name
136
- Brut.container.instrumentation.span(component_name) do |span|
137
+ Brut.container.instrumentation.span("component #{component_name}") do |span|
137
138
  if component_instance.kind_of?(Class)
138
139
  if !component_instance.ancestors.include?(Brut::FrontEnd::Component)
139
140
  raise ArgumentError,"#{component_instance} is not a component and cannot be created"
@@ -141,9 +142,9 @@ class Brut::FrontEnd::Component
141
142
  component_instance = Thread.current.thread_variable_get(:request_context).
142
143
  then { |request_context| request_context.as_constructor_args(component_instance,request_params: nil)
143
144
  }.then { |constructor_args| component_instance.new(**constructor_args) }
144
- span.add_attributes("global_component" => true, "class" => component_instance.class.name)
145
+ span.add_prefixed_attributes("brut", "global_component" => true)
145
146
  else
146
- span.add_attributes("global_component" => false, "class" => component_instance.class.name)
147
+ span.add_prefixed_attributes("brut", "global_component" => false)
147
148
  end
148
149
  if !block.nil?
149
150
  component_instance.yielded_block = block
@@ -35,7 +35,7 @@ class Brut::FrontEnd::Form
35
35
  self.class.input_definitions.key?(key)
36
36
  }
37
37
  if unknown_params.any?
38
- Brut.container.instrumentation.add_attributes(ignored_unknown_params: unknown_params)
38
+ Brut.container.instrumentation.add_attributes(prefix: :brut, ignored_unknown_params: unknown_params.join(","))
39
39
  end
40
40
  @params = params.except(*unknown_params).map { |name,value|
41
41
  input_definition = begin
@@ -132,7 +132,18 @@ class Brut::FrontEnd::Form
132
132
  # values are arrays of {Brut::FrontEnd::Forms::ValidityState} instances.
133
133
  #
134
134
  # @param [true|false] server_side_only if true, only server side constraints are returned.
135
- # @return [Hash] map of input names to arrays of validity states
135
+ # @return [Hash<String|Array[2]>] a map of input names to arrays of constraint violations. The first element in the
136
+ # array is the validity state for the input, which is a {Brut::FrontEnd::Forms::ValidityState} instance. The second
137
+ # element is the index of the input in the array. This index is used when you have more than one field with the same
138
+ # name.
139
+ #
140
+ # @example iterationg
141
+ # form.constraint_violations.each do |input_name, (constraint_violations,index)|
142
+ # # input_name is the input's name, e.g. "email"
143
+ # # constraint_violations is an array of {Brut::FrontEnd::Forms::ValidityState} instances, one for each
144
+ # # problem with the field's value
145
+ # # index is the index of the input in the array, e.g. 0 for the first email field, 1 for the second, etc.
146
+ # end
136
147
  #
137
148
  def constraint_violations(server_side_only: false)
138
149
  @inputs.map { |input_name, inputs|
@@ -179,15 +190,17 @@ class Brut::FrontEnd::Form
179
190
  private
180
191
 
181
192
  def convert_to_string_or_nil(hash)
193
+ converted_hash = {}
182
194
  hash.each do |key,value|
195
+ key = key.to_s
183
196
  case value
184
- in Hash then convert_to_string_or_nil(value)
185
- in String then hash[key] = RichString.new(value).to_s_or_nil
186
- in Numeric then hash[key] = value.to_s
187
- in TrueClass then hash[key] = "true"
188
- in FalseClass then hash[key] = "false"
189
- in NilClass then # it's fine
190
- in Array then hash[key] = value
197
+ in Hash then converted_hash[key] = convert_to_string_or_nil(value)
198
+ in String then converted_hash[key] = RichString.new(value).to_s_or_nil
199
+ in Numeric then converted_hash[key] = value.to_s
200
+ in TrueClass then converted_hash[key] = "true"
201
+ in FalseClass then converted_hash[key] = "false"
202
+ in NilClass then converted_hash[key] = nil
203
+ in Array then converted_hash[key] = value
191
204
  else
192
205
  if Brut.container.project_env.test?
193
206
  raise ArgumentError, "Got #{value.class} for #{key} in params hash, which is not expected"
@@ -196,5 +209,6 @@ private
196
209
  end
197
210
  end
198
211
  end
212
+ converted_hash
199
213
  end
200
214
  end
@@ -4,7 +4,7 @@ class Brut::FrontEnd::Handlers::CspReportingHandler < Brut::FrontEnd::Handler
4
4
  def handle(body:)
5
5
  begin
6
6
  parsed = JSON.parse(body.read)
7
- Brut.container.instrumentation.add_attributes(parsed)
7
+ Brut.container.instrumentation.add_attributes(parsed.merge(prefix: "brut.csp-reporting"))
8
8
  rescue => ex
9
9
  Brut.container.instrumentation.record_exception(ex)
10
10
  end
@@ -8,8 +8,13 @@ class Brut::FrontEnd::Handlers::LocaleDetectionHandler < Brut::FrontEnd::Handler
8
8
  def handle(body:,session:)
9
9
  begin
10
10
  parsed = JSON.parse(body.read)
11
- Brut.container.instrumentation.add_attributes(parsed_body: parsed)
12
- Brut.container.instrumentation.add_attributes(parsed_class: parsed.class)
11
+
12
+ Brut.container.instrumentation.add_attributes(
13
+ prefix: "brut.locale-detection",
14
+ parsed_body: parsed,
15
+ parsed_class: parsed.class
16
+ )
17
+
13
18
  if parsed.kind_of?(Hash)
14
19
  locale = parsed["locale"]
15
20
  timezone = parsed["timeZone"]
@@ -7,7 +7,7 @@ class Brut::FrontEnd::Middlewares::AnnotateBrutOwnedPaths < Brut::FrontEnd::Midd
7
7
  end
8
8
  def call(env)
9
9
  if env["PATH_INFO"] =~ /^\/__brut\//
10
- Brut.container.instrumentation.add_attributes("brut.owned_path" => true)
10
+ Brut.container.instrumentation.add_attributes(prefix: "brut", owned_path: true)
11
11
  env["brut.owned_path"] = true
12
12
  end
13
13
  @app.call(env)
@@ -3,9 +3,33 @@ class Brut::FrontEnd::Middlewares::OpenTelemetrySpan < Brut::FrontEnd::Middlewar
3
3
  @app = app
4
4
  end
5
5
  def call(env)
6
- path = env["REQUEST_PATH"]
7
- method = env["REQUEST_METHOD"]
8
- Brut.container.instrumentation.span("HTTP.#{method}.#{path}") do |span|
6
+ otel_standard_attributes = {
7
+ "http.request.method" => env["REQUEST_METHOD"],
8
+ "url.path" => env["PATH_INFO"],
9
+ "url.query" => env["QUERY_STRING"],
10
+ "url.scheme" => env["rack.url_scheme"],
11
+ "url.full" => "#{env["rack.url_scheme"]}://#{env["HTTP_HOST"]}#{env["REQUEST_URI"]}",
12
+ "server.address" => env["HTTP_HOST"],
13
+ "user_agent.original" => env["HTTP_USER_AGENT"],
14
+ "network.peer.ip" => env["REMOTE_ADDR"],
15
+ "network.peer.port" => env["REMOTE_PORT"],
16
+ "network.protocol.version" => env["HTTP_VERSION"],
17
+ }.merge(
18
+ "http.request.header.accept-language" => env["HTTP_ACCEPT_LANGUAGE"],
19
+ "http.request.header.referer" => env["HTTP_REFERER"],
20
+ "http.request.header.user-agent" => env["HTTP_USER_AGENT"],
21
+ "http.request.header.accept" => env["HTTP_ACCEPT"],
22
+ ).delete_if { |_,v| v.nil? || v.empty? }
23
+
24
+ span_name = if env["PATH_INFO"] =~ /^\/js\//
25
+ "HTTP #{env['REQUEST_METHOD']} JS"
26
+ elsif env["PATH_INFO"] =~ /^\/css\//
27
+ "HTTP #{env['REQUEST_METHOD']} CSS"
28
+ else
29
+ "HTTP #{env['REQUEST_METHOD']}"
30
+ end
31
+ Brut.container.tracer.in_span(span_name,kind: :server, attributes: otel_standard_attributes) do |span|
32
+ env["brut.otel.root_span"] = span
9
33
  @app.call(env)
10
34
  end
11
35
  end
@@ -5,7 +5,6 @@
5
5
  class Brut::FrontEnd::RouteHooks::LocaleDetection < Brut::FrontEnd::RouteHook
6
6
  def before(session:,env:)
7
7
  http_accept_language = Brut::I18n::HTTPAcceptLanguage.from_header(env["HTTP_ACCEPT_LANGUAGE"])
8
- Brut.container.instrumentation.add_attributes(http_accept_language:)
9
8
  if !session.http_accept_language.known?
10
9
  session.http_accept_language = http_accept_language
11
10
  end
@@ -20,10 +19,10 @@ class Brut::FrontEnd::RouteHooks::LocaleDetection < Brut::FrontEnd::RouteHook
20
19
  end
21
20
  end
22
21
  if best_locale
23
- Brut.container.instrumentation.add_attributes(best_locale:)
22
+ Brut.container.instrumentation.add_attributes(prefix: "brut.locale-detection", best_locale:)
24
23
  ::I18n.locale = best_locale
25
24
  else
26
- Brut.container.instrumentation.add_attributes(best_locale: false)
25
+ Brut.container.instrumentation.add_attributes(prefix: "brut.locale-detection", best_locale: false)
27
26
  end
28
27
  continue
29
28
  end
@@ -3,9 +3,7 @@ class Brut::Instrumentation::OpenTelemetry
3
3
  #
4
4
  # @param [String] name the name of the span. Should be specific to the code being wrapped, but not contain dynamic information. For
5
5
  # example, you could call this the method name, but should not include parameters in the name.
6
- # @param [Hash<String|Symbol,Object>] attributes Hash of attributes to include in this span. This is as if you called
7
- # {Brut::Instrumentation::OpenTelemetry::Span#add_attributes} as the first line of the block. See that method for more details on
8
- # the contents of this hash.
6
+ # @param [Hash<String|Symbol,Object>] attributes Hash of attributes to include in this span. This is as if you called {Brut::Instrumentation::OpenTelemetry::Span#add_attributes} as the first line of the block. There is a special attribute named `prefix:` that will control how attributes are prefixed. If it is missing, the app's configured OTel prefix is used. If it is sent to `false`, no prefixing is done. Otherwise, the provided value is used as the prefix. Generally, you don't want to set this so your app's prefix is used. Also note that the keys and values are automatically converted to primitive types, so you can use whatever makes sense. Just know that for rich objects `to_s` will be called.
9
7
  #
10
8
  # @yield [Brut::Instrumentation::OpenTelemetry::Span] executes this block in the context of a new OpenTelemetry span. yields
11
9
  # the span so you can call further methods on it.
@@ -16,9 +14,9 @@ class Brut::Instrumentation::OpenTelemetry
16
14
  #
17
15
  def span(name,**attributes,&block)
18
16
  result = nil
19
- Brut.container.tracer.in_span(name) do |span|
17
+ normalized_attributes = NormalizedAttributes.new(:detect,attributes).to_h
18
+ Brut.container.tracer.in_span(name, attributes: normalized_attributes) do |span|
20
19
  wrapped_span = Span.new(span)
21
- wrapped_span.add_attributes(attributes)
22
20
  begin
23
21
  result = block.(wrapped_span)
24
22
  rescue => ex
@@ -46,22 +44,32 @@ class Brut::Instrumentation::OpenTelemetry
46
44
  current_span.record_exception(ex,attributes: NormalizedAttributes.new(nil,attributes).to_h)
47
45
  end
48
46
 
49
- # Adds attributes to the current span
47
+ # Adds attributes to the span, converting the hash or keyword arguments to strings. This will use
48
+ # the app's Otel prefix for all attributes, so you do not have to prefix them.
49
+ # If you need to set standard attributes, you should use {Brut::Instrumentation::OpenTelemetry::Span#add_prefixed_attributes} instead.
50
50
  # @param [Hash] attributes any attributes to attach to the event.
51
51
  def add_attributes(attributes)
52
52
  current_span = OpenTelemetry::Trace.current_span
53
- current_span.add_attributes(NormalizedAttributes.new(nil,attributes).to_h)
53
+ current_span.add_attributes(NormalizedAttributes.new(:detect,attributes).to_h)
54
54
  end
55
55
 
56
+ private
57
+
56
58
  class NormalizedAttributes
57
59
  def initialize(prefix,attributes)
58
- prefix = if prefix
59
- "#{prefix}."
60
- else
61
- ""
62
- end
60
+ if prefix == :detect
61
+ prefix = attributes.delete(:prefix)
62
+ if prefix.nil?
63
+ prefix = Brut.container.otel_attribute_prefix
64
+ end
65
+ end
66
+ prefix_string = if prefix
67
+ "#{prefix}."
68
+ else
69
+ ""
70
+ end
63
71
  @attributes = (attributes || {}).map { |key,value|
64
- [ "#{prefix}#{key}", normalize_value(value) ]
72
+ [ "#{prefix_string}#{key}", normalize_value(value) ]
65
73
  }.to_h
66
74
  end
67
75
 
@@ -82,20 +90,23 @@ class Brut::Instrumentation::OpenTelemetry
82
90
  value.to_s
83
91
  end
84
92
  end
93
+
85
94
  end
86
95
 
87
96
  class Span < SimpleDelegator
88
97
 
89
- # Adds attributes to the span, converting the hash or keyword arguments to strings.
98
+ # Adds attributes to the span, converting the hash or keyword arguments to strings. This will use
99
+ # the app's Otel prefix for all attributes, so you do not have to prefix them.
100
+ # If you need to set standard attributes, you should use {#add_prefixed_attributes} instead.
90
101
  #
91
102
  # @param [Hash] attributes a hash of the attributes to add. Keys will be converted to strings via `to_s`.
92
103
  # Values will be converted via {Brut::Instrumentation::OpenTelemetry::NormalizedAttributes}, which preserves strings, numbers, and
93
104
  # booleans, and converts the rest to strings via `to_s`.
94
105
  def add_attributes(attributes)
95
- add_prefixed_attributes(nil,attributes)
106
+ add_prefixed_attributes(:detect,attributes)
96
107
  end
97
108
 
98
- # Adds attributes to the span, prefixing each key with the given prefix, then converting the hash or keyword arguments to strings.
109
+ # Adds attributes to the span, prefixing each key with the given prefix, then converting the hash or keyword arguments to strings. For example, if the prefix is 'my_app' and you add the attributes 'type' and 'reason', the actual attribute names will be 'my_app.type' and 'my_app.reason'.
99
110
  #
100
111
  # @see #add_attributes
101
112
  def add_prefixed_attributes(prefix,attributes)
@@ -2,6 +2,7 @@ module Brut::Instrumentation
2
2
  autoload(:OpenTelemetry,"brut/instrumentation/open_telemetry")
3
3
  autoload(:LoggerSpanExporter,"brut/instrumentation/logger_span_exporter")
4
4
 
5
+ # Convenience method to add attributes to create a span without accessing the instrumentation instance directly.
5
6
  def span(name,**attributes,&block)
6
7
  Brut.container.instrumentation.span(name,**attributes,&block)
7
8
  end
@@ -65,19 +65,28 @@ module Brut::SinatraHelpers
65
65
  Brut.container.routing.register_page(path)
66
66
 
67
67
  get path do
68
- route = Brut.container.routing.for(path: path,method: :get)
69
- page_class = route.handler_class
70
- Brut.container.instrumentation.span(page_class.name, path: path) do |span|
68
+ brut_route = Brut.container.routing.for(path: path,method: :get)
69
+ page_class = brut_route.handler_class
70
+ path_template = brut_route.path_template
71
+
72
+ root_span = env["brut.otel.root_span"]
73
+ if root_span
74
+ root_span.name = "GET #{path_template}"
75
+ root_span.add_attributes("http.route" => path_template)
76
+ end
77
+
78
+ Brut.container.instrumentation.span(page_class.name) do |span|
79
+ span.add_prefixed_attributes("brut", type: :page, class: page_class)
71
80
  request_context = Thread.current.thread_variable_get(:request_context)
72
81
  constructor_args = request_context.as_constructor_args(
73
82
  page_class,
74
83
  request_params: params,
75
- route: route,
84
+ route: brut_route,
76
85
  )
77
- span.add_prefixed_attributes("initializer.args", constructor_args.map { |k,v| [k.to_s,v.class.name] }.to_h)
86
+ span.add_prefixed_attributes("brut.initializer.args", constructor_args.map { |k,v| [k.to_s,v.class.name] }.to_h)
78
87
  page_instance = page_class.new(**constructor_args)
79
88
  result = page_instance.handle!
80
- span.add_attributes(result_class: result.class)
89
+ span.add_prefixed_attributes("brut", result_class: result.class)
81
90
  case result
82
91
  in URI => uri
83
92
  redirect to(uri.to_s)
@@ -148,37 +157,55 @@ module Brut::SinatraHelpers
148
157
  # This must be re-looked up per-request do allow reloading to work
149
158
  brut_route = Brut.container.routing.for(path:,method:)
150
159
 
160
+ path_template = brut_route.path_template
161
+
162
+ root_span = env["brut.otel.root_span"]
163
+ if root_span
164
+ root_span.name = "#{method} #{path_template}"
165
+ root_span.add_attributes("http.route" => path_template)
166
+ end
167
+
151
168
  handler_class = brut_route.handler_class
152
- form_class = brut_route.respond_to?(:form_class) ? brut_route.form_class : nil
153
-
154
- request_context = Thread.current.thread_variable_get(:request_context)
155
- handler = handler_class.new
156
- form = if form_class.nil?
157
- nil
158
- else
159
- form_class.new(params: params)
160
- end
161
-
162
- process_args = request_context.as_method_args(handler,:handle,request_params: params,form: form,route:brut_route)
163
-
164
- result = handler.handle!(**process_args)
165
-
166
- case result
167
- in URI => uri
168
- redirect to(uri.to_s)
169
- in Brut::FrontEnd::Component => component_instance
170
- render_html(component_instance).to_s
171
- in [ Brut::FrontEnd::Component => component_instance, Brut::FrontEnd::HttpStatus => http_status ]
172
- [
173
- http_status.to_i,
174
- render_html(component_instance).to_s,
175
- ]
176
- in Brut::FrontEnd::HttpStatus => http_status
177
- http_status.to_i
178
- in Brut::FrontEnd::Download => download
179
- [ 200, download.headers, download.data ]
180
- else
181
- raise NoMatchingPatternError, "Result from #{handler.class}'s handle! method was a #{result.class}, which cannot be used to understand the response to generate"
169
+
170
+ Brut.container.instrumentation.span(handler_class.name) do |span|
171
+
172
+ form_class = brut_route.respond_to?(:form_class) ? brut_route.form_class : nil
173
+
174
+ span.add_prefixed_attributes("brut",
175
+ type: form_class ? :form : :action,
176
+ class: handler_class,
177
+ form_class: form_class,
178
+ )
179
+
180
+ request_context = Thread.current.thread_variable_get(:request_context)
181
+ handler = handler_class.new
182
+ form = if form_class.nil?
183
+ nil
184
+ else
185
+ form_class.new(params: params)
186
+ end
187
+
188
+ process_args = request_context.as_method_args(handler,:handle,request_params: params,form: form,route:brut_route)
189
+
190
+ result = handler.handle!(**process_args)
191
+
192
+ case result
193
+ in URI => uri
194
+ redirect to(uri.to_s)
195
+ in Brut::FrontEnd::Component => component_instance
196
+ render_html(component_instance).to_s
197
+ in [ Brut::FrontEnd::Component => component_instance, Brut::FrontEnd::HttpStatus => http_status ]
198
+ [
199
+ http_status.to_i,
200
+ render_html(component_instance).to_s,
201
+ ]
202
+ in Brut::FrontEnd::HttpStatus => http_status
203
+ http_status.to_i
204
+ in Brut::FrontEnd::Download => download
205
+ [ 200, download.headers, download.data ]
206
+ else
207
+ raise NoMatchingPatternError, "Result from #{handler.class}'s handle! method was a #{result.class}, which cannot be used to understand the response to generate"
208
+ end
182
209
  end
183
210
  end
184
211
  end
data/lib/brut/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Brut
2
2
  # @!visibility private
3
- VERSION = "0.0.10"
3
+ VERSION = "0.0.11"
4
4
  end
@@ -4,7 +4,13 @@ module Sequel
4
4
  module BrutInstrumentation
5
5
  # @!visibility private
6
6
  def log_connection_yield(sql,conn,args=nil)
7
- Brut.container.instrumentation.span("SQL", sql: sql) do |span|
7
+ otel_attributes = {
8
+ "db.system" => "sql",
9
+ "db.system.name" => "postgresql",
10
+ "db.query.text" => sql,
11
+ "prefix" => false,
12
+ }
13
+ Brut.container.instrumentation.span("SQL", **otel_attributes) do |span|
8
14
  super
9
15
  end
10
16
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Bryant Copeland
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-02 00:00:00.000000000 Z
10
+ date: 2025-04-04 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: irb